知識庫 / Testing RSS 訂閱

Spring OAuth2 訪問控制測試

Spring Security,Testing
HongKong
5
11:42 AM · Dec 06 ,2025

1. 概述

本教程將探討在具有 OAuth2 安全性的 Spring 應用中使用 Mocked 身份驗證來測試訪問控制規則的選項。

我們將使用 MockMvc 請求後處理程序、WebTestClient 變異器以及來自 spring-security-testspring-addons 的測試註解。

2. 使用 Spring-Addons 的原因

在 OAuth2 領域中,`spring-security-test 僅提供基於 MockMvcWebTestClient 的請求後處理器和轉換器,這通常僅適用於 @Controller 的測試。但對於測試服務 (@Service) 或倉庫 (@Repository) 的方法安全性(如 @PreAuthorize、@PostFilter 等)則存在問題。

通過使用諸如 @WithJwt@WithOidcLogin 這樣的註解,我們可以對任何類型的 @Component 在 Servlet 和響應式應用程序中的單元測試進行安全上下文的模擬。因此,我們將使用 spring-addons-oauth2-test 在某些測試中使用它:它為大多數 Spring OAuth2 Authentication 實現提供了這些註解。

3. 測試內容是什麼?

配套的 GitHub 倉庫 包含兩個資源服務器,共享以下功能:

  • 使用 JWT 解碼器進行安全保護(而不是不透明的令牌內省)
  • 需要 ROLE_AUTHORIZED_PERSONNEL 權限才能訪問 /secured-route/secured-method
  • 如果身份驗證缺失或無效(過期、頒發者錯誤等),則返回 401 錯誤;如果訪問被拒絕(缺少角色),則返回 403 錯誤
  • 使用 Java 配置定義訪問控制(Servlet 和響應式應用分別使用 requestMatcherpathMatcher),以及方法安全
  • 使用身份驗證上下文中的數據構建響應負載

為了説明 Servlet 和響應式測試 API 之間的細微差異,一個為 Servlet,另一個為響應式應用。

在本文中,我們將重點測試單元測試和集成測試中訪問控制規則,並斷言響應的 HTTP 狀態與根據模擬的用户身份的預期匹配,或者在單元測試其他@Component時,當@Controller@Service@Repository(使用@PreAuthorize@PostFilter等)時,拋出異常

所有測試都無需授權服務器即可通過,但如果想要啓動用於測試的資源服務器,並使用 Postman 等工具對其進行查詢,則需要一個服務器正在運行。 提供的 Docker Compose 文件可用於快速啓動 Keycloak 實例:

  • 管理控制枱可用於:http://localhost:8080/admin/master/console/#/baeldung
  • 管理員帳户為 admin / admin
  • 已創建了一個名為 baeldung 的領域,其中包含一個秘密客户端 (baeldung_confidential / secret) 和兩個用户 (authorizedforbidden,兩者都使用 secret 作為密鑰)

4. 使用模擬認證進行單元測試

通過“單元測試”,我們指的是在一個隔離的 @Component 中進行測試,不依賴任何其他依賴項(我們將使用模擬)。被測試的 @Component 可以是 @Controller@WebMvcTest@WebFluxTest 中,也可以是任何其他受保護的 @Service@Repository 等,在標準的 JUnit 測試中。

MockMvcWebTestClient 會忽略 Authorization 頭部,因此無需提供有效的訪問令牌。當然,我們可以實例化或模擬任何認證實現,並在每個測試的開頭手動創建安全上下文,但這過於繁瑣。相反,我們將使用 spring-security-testMockMvc 請求後處理程序、WebTestClient 變體或 spring-addons 註解,以便使用我們選擇的模擬 Authentication 實例填充測試安全上下文.

我們將使用 @WithMockUser 僅用於驗證它是否構建了 UsernamePasswordAuthenticationToken 實例,因為 OAuth2 運行時配置經常會將其他類型的 Authentication 放入安全上下文中:

  • JwtAuthenticationToken 用於資源服務器,並帶有 JWT 解碼器
  • BearerTokenAuthentication 用於資源服務器,進行訪問令牌隱式獲取 (opaqueToken)
  • OAuth2AuthenticationToken 用於具有 oauth2Login 配置的客户端
  • 如果我們在自定義認證轉換器中返回另一個 Authentication 實例,則可以返回任何內容。因此,從技術上講,OAuth2 認證轉換器可以返回 UsernamePasswordAuthenticationToken 實例,並使用 @WithMockUser 在測試中,但這是一種不太自然的選擇,我們不會在這裏使用它。

4.1. 重要提示

<em>MockMvc</em> 的預處理器和 <em>WebTestClient</em> 的修改器不使用安全配置中定義的 Bean 來構建測試 <em>Authentication</em> 實例。 因此,使用 <em>SecurityMockMvcRequestPostProcessors.jwt()</em><em>SecurityMockServerConfigurers.mockJwt()</em> 構建 <em>Authentication</em> 實例並不會影響身份驗證名稱和權限。 我們必須自己設置名稱和權限,使用專門的方法。

與之形成對比的是,spring-addons 註解背後的工廠會掃描測試上下文以查找身份驗證轉換器,並在找到它時使用它。 因此,在使用 <em>@WithJwt</em> 時,重要的是將自定義 <em>JwtAuthenticationConverter</em> 暴露為 Bean(而不是僅僅將其內聯為 security conf 中的 lambda 表達式):

@Configuration
@EnableMethodSecurity
@EnableWebSecurity
static class SecurityConf {
    @Bean
    SecurityFilterChain filterChain(HttpSecurity http, Converter<Jwt, AbstractAuthenticationToken> authenticationConverter) throws Exception {
        http.oauth2ResourceServer(resourceServer -> resourceServer.jwt(jwtResourceServer -> jwtResourceServer.jwtAuthenticationConverter(authenticationConverter)));
        ...
    }

    @Bean
    JwtAuthenticationConverter authenticationConverter(Converter<Jwt, Collection<GrantedAuthority>> authoritiesConverter) {
        final var authenticationConverter = new JwtAuthenticationConverter();
        authenticationConverter.setPrincipalClaimName(StandardClaimNames.PREFERRED_USERNAME);
        authenticationConverter.setJwtGrantedAuthoritiesConverter(authoritiesConverter);
        return authenticationConverter;
    }
}

值得注意的是,認證轉換器被暴露為一個 @Bean,並被明確地注入到安全過濾器鏈中。 這樣,@WithJwt 背後工廠就可以利用它來從聲明中構建 Authentication,就像在運行時使用真實令牌時所做的那樣。

此外,在高級情況下,當認證轉換器返回的內容不是 JwtAuthenticationToken(或者資源服務器在進行令牌內省時,返回 BearerTokenAuthentication)時,僅 Spring Addons 的測試註解才會構建期望的 Authentication 類型。

4.2. 測試環境搭建

對於@Controller單元測試,我們應該使用@WebMvcTest來裝飾測試類,用於servlet應用,以及@WebFluxTest用於響應式應用。

Spring會自動注入MockMvcWebTestClient,並且由於我們正在編寫控制器單元測試,因此我們將模擬MessageService

以下是一個在servlet應用中,空@Controller單元測試的示例:

@WebMvcTest(controllers = GreetingController.class)
class GreetingControllerTest {

    @MockBean
    MessageService messageService;

    @Autowired
    MockMvc mockMvc;

    //...
}
這是一個在反應式應用中,空 @Controller 單元測試的樣子:
@WebFluxTest(controllers = GreetingController.class) class GreetingControllerTest { private static final AnonymousAuthenticationToken ANONYMOUS = new AnonymousAuthenticationToken("anonymous", "anonymousUser", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS")); @MockBean MessageService messageService; @Autowired WebTestClient webTestClient; //... }

現在,讓我們看看如何斷言 HTTP 狀態碼與我們之前設定的規範相符。

4.3. 使用 MockMvc 後處理程序進行單元測試

為了將測試安全上下文填充為 JwtAuthenticationToken,這是用於具有 JWT 解碼器的資源服務器的默認 Authentication 類型,我們將使用 jwt 後處理程序,用於 MockMvc 請求:

import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors;

讓我們來看幾個使用 MockMvc 以及根據端點和模擬的身份驗證進行響應狀態斷言的示例:

@Test
void givenRequestIsAnonymous_whenGetGreet_thenUnauthorized() throws Exception {
    mockMvc.perform(get("/greet").with(SecurityMockMvcRequestPostProcessors.anonymous()))
      .andExpect(status().isUnauthorized());
}

在上述內容中,我們確保匿名請求無法收到問候語,並且會正確返回一個 401 錯誤碼。

現在,讓我們看看請求如何根據端點的安全規則以及分配給測試 JwtAuthenticationToken 的管理員,返回 200 Ok 或 403 Forbidden。

@Test
void givenUserIsGrantedWithRoleAuthorizedPersonnel_whenGetSecuredRoute_thenOk() throws Exception {
    var secret = "Secret!";
    when(messageService.getSecret()).thenReturn(secret);

    mockMvc.perform(get("/secured-route").with(SecurityMockMvcRequestPostProcessors.jwt()
      .authorities(new SimpleGrantedAuthority("ROLE_AUTHORIZED_PERSONNEL"))))
        .andExpect(status().isOk())
        .andExpect(content().string(secret));
}

@Test
void givenUserIsNotGrantedWithRoleAuthorizedPersonnel_whenGetSecuredRoute_thenForbidden() throws Exception {
    mockMvc.perform(get("/secured-route").with(SecurityMockMvcRequestPostProcessors.jwt()
      .authorities(new SimpleGrantedAuthority("admin"))))
        .andExpect(status().isForbidden());
}

4.4. 使用 WebTestClient 變異器進行單元測試

在響應式資源服務器中,安全上下文中 Authentication 類型的實現與 Servlet 中的實現相同:JwtAuthenticationToken。因此,我們將使用 mockJwt 變異器用於 WebTestClient

import org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers;

<em >MockMvc</em > 後處理器不同,沒有匿名 <em >WebTestClient</em > 修改器。但是,可以通過輕鬆定義匿名 <em >Authentication</em > 實例並使用它通過泛型 <em >mockAuthentication</em > 修改器:

private static final AnonymousAuthenticationToken ANONYMOUS = new AnonymousAuthenticationToken(
    "anonymous", "anonymousUser", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS"));

@Test
void givenRequestIsAnonymous_whenGetGreet_thenUnauthorized() throws Exception {
    webTestClient.mutateWith(SecurityMockServerConfigurers.mockAuthentication(ANONYMOUS))
        .get()
        .uri("/greet")
        .exchange()
        .expectStatus()
        .isUnauthorized();
}

@Test
void givenUserIsAuthenticated_whenGetGreet_thenOk() throws Exception {
    var greeting = "Whatever the service returns";
    when(messageService.greet()).thenReturn(Mono.just(greeting));

    webTestClient.mutateWith(SecurityMockServerConfigurers.mockJwt().authorities(List.of(
      new SimpleGrantedAuthority("admin"), 
      new SimpleGrantedAuthority("ROLE_AUTHORIZED_PERSONNEL")))
        .jwt(jwt -> jwt.claim(StandardClaimNames.PREFERRED_USERNAME, "ch4mpy")))
        .get()
        .uri("/greet")
        .exchange()
        .expectStatus()
        .isOk()
        .expectBody(String.class)
        .isEqualTo(greeting);

    verify(messageService, times(1)).greet();
}

@Test
void givenUserIsNotGrantedWithRoleAuthorizedPersonnel_whenGetSecuredRoute_thenForbidden() throws Exception {
    webTestClient.mutateWith(mockJwt().authorities(new SimpleGrantedAuthority("admin")))
        .get()
        .uri("/secured-route")
        .exchange()
        .expectStatus()
        .isForbidden();
}
<div>
  <h1>Introduction</h1>
  <p>This document provides an overview of the new API. It covers key features, usage examples, and troubleshooting tips.</p>

  <h2>Key Features</h2>
  <ul>
    <li><strong>Data Validation:</strong> Ensures data integrity by validating input against predefined rules.</li>
    <li><strong>Asynchronous Operations:</strong> Enables non-blocking operations for improved performance.</li>
    <li><strong>Error Handling:</strong> Provides robust error handling mechanisms for graceful failure.</li>
  </ul>

  <h2>Usage Examples</h2>
  <pre>
    <code>
    // Example JavaScript code
    function fetchData(url) {
      // Make an HTTP request
      fetch(url)
        .then(response => response.json())
        .then(data => {
          console.log(data);
        })
        .catch(error => {
          console.error("Error fetching data:", error);
        });
    }

    // Call the function
    fetchData("https://example.com/api/data");
    </code>
  </pre>

  <p>This example demonstrates how to use the API to retrieve data from a remote server.</p>

  <h2>Troubleshooting</h2>
  <h3>Common Issues</h3>
  <ul>
    <li><strong>Connection Errors:</strong> Verify network connectivity and DNS resolution.</li>
    <li><strong>Authentication Errors:</strong> Double-check API keys and credentials.</li>
    <li><strong>Invalid Data:</strong> Ensure data conforms to the expected format.</li>
  </ul>
</div>

4.5. 使用註解對控制器進行單元測試(來自 Spring-Addons

我們可以以完全相同的方式在servlet和響應式應用中使用測試註解。

所有我們需要的只是添加對 spring-addons-oauth2-test 的依賴:

<dependency>
    <groupId>com.c4-soft.springaddons</groupId>
    <artifactId>spring-addons-oauth2-test</artifactId>
    <version>7.6.12</version>
    <scope>test</scope>
</dependency>

該庫包含許多註釋,涵蓋以下用例:

  • @WithMockAuthentication 通常足以用於測試基於角色的訪問控制:它旨在將權限作為參數傳遞,但也接受用户名和要模擬的類型以進行 AuthenticationPrincipal 的模擬。
  • @WithJwt 用於測試使用 JWT 解碼器的資源服務器。它依賴於一個工廠,該工廠從安全配置中選擇 Converter<Jwt, ? extends AbstractAthenticationToken> (或 Converter<Jwt, ? extends Mono<? extends AbstractAuthenticationToken>> 在反應式應用程序中) ,並使用測試類路徑上的 JSON 負載。這提供了對聲明的完全控制,並提供與運行時相同的身份驗證實例,用於相同的 JWT 負載。
  • @WithOpaqueToken@WithJwt 相同,但用於具有令牌內省的資源服務器:它依賴於一個工廠選擇 OpaqueTokenAuthenticationConverter (或 ReactiveOpaqueTokenAuthenticationConverter)。
  • @WithOAuth2Login@WithOidcLogin 將在我們要測試 OAuth2 客户端和登錄時使用。

在開始測試之前,我們將定義 JSON 文件作為測試資源。這旨在模擬訪問令牌(或內省響應)的 JSON 負載(針對代表性用户personaspersonae)。我們可以使用像 https://jwt.io 這樣的工具複製真實令牌的負載。

Ch4mpy 將是我們的測試用户,具有 AUTHORIZED_PERONNEL 角色:

{
  "iss": "https://localhost:8443/realms/master",
  "sub": "281c4558-550c-413b-9972-2d2e5bde6b9b",
  "iat": 1695992542,
  "exp": 1695992642,
  "preferred_username": "ch4mpy",
  "realm_access": {
    "roles": [
      "admin",
      "ROLE_AUTHORIZED_PERSONNEL"
    ]
  },
  "email": "[email protected]",
  "scope": "openid email"
}

我們還將定義一個沒有 AUTHORIZED_PERONNEL 角色的用户:

{
  "iss": "https://localhost:8443/realms/master",
  "sub": "2d2e5bde6b9b-550c-413b-9972-281c4558",
  "iat": 1695992551,
  "exp": 1695992651,
  "preferred_username": "tonton-pirate",
  "realm_access": {
    "roles": [
      "uncle",
      "skipper"
    ]
  },
  "email": "[email protected]",
  "scope": "openid email"
}

現在,我們可以從測試體中移除身份欺構,用註解來裝飾測試方法。為了演示目的,我們將使用 @WithMockAuthentication@WithJwt 兩個註解,但實際測試中只需要一個就足夠了。我們通常會在需要定義僅指定權限或名稱時選擇第一個,而在需要處理多個聲明時選擇第二個。

@Test
@WithAnonymousUser
void givenRequestIsAnonymous_whenGetSecuredMethod_thenUnauthorized() throws Exception {
    api.perform(get("/secured-method"))
        .andExpect(status().isUnauthorized());
}

@Test
@WithMockAuthentication({ "admin", "ROLE_AUTHORIZED_PERSONNEL" })
void givenUserIsGrantedWithRoleAuthorizedPersonnel_whenGetSecuredMethod_thenOk() throws Exception {
    final var secret = "Secret!";
    when(messageService.getSecret()).thenReturn(secret);

    api.perform(get("/secured-method"))
        .andExpect(status().isOk())
        .andExpect(content().string(secret));
}

@Test
@WithMockAuthentication({ "admin" })
void givenUserIsNotGrantedWithRoleAuthorizedPersonnel_whenGetSecuredMethod_thenForbidden() throws Exception {
    api.perform(get("/secured-method"))
        .andExpect(status().isForbidden());
}

@Test
@WithJwt("ch4mpy.json")
void givenUserIsCh4mpy_whenGetSecuredMethod_thenOk() throws Exception {
    final var secret = "Secret!";
    when(messageService.getSecret()).thenReturn(secret);

    api.perform(get("/secured-method"))
        .andExpect(status().isOk())
        .andExpect(content().string(secret));
}

@Test
@WithJwt("tonton-pirate.json")
void givenUserIsTontonPirate_whenGetSecuredMethod_thenForbidden() throws Exception {
    api.perform(get("/secured-method"))
        .andExpect(status().isForbidden());
}

註釋與BDD範式非常契合

  • 前提條件(Given)位於文本上下文中(註釋裝飾測試用例)
  • 僅測試代碼執行(When)和結果斷言(Then)位於測試主體中

4.6. 單元測試受保護的方法(@Service 或 @Repository)

當測試 @Controller 時,選擇請求 MockMvc 後處理程序(或 WebTestClient 變體)與註解之間的選擇主要取決於團隊偏好,但為了單元測試 MessageService::getSecret 的訪問控制,spring-security-test 已經不再適用,我們需要使用 spring-addons 註解。

以下是 JUnit 設置:

  • 使用 @ExtendWith(SpringExtension.class) 激活 Spring 自動裝配
  • 導入並自動裝配 MessageService 以獲取經過Instrumentation 的實例
  • 如果使用 @WithJwt,則需要導入包含 JwtAuthenticationConverter 以及 AuthenticationFactoriesTestConf 的配置。否則,使用 @EnableMethodSecurity 裝飾測試即可。

我們將斷言 MessageService 在用户缺少 ROLE_AUTHORIZED_PERSONNEL 權限時,每次都會拋出異常。

以下是一個在 Servlet 應用程序中測試 @Service 的完整單元測試:

@ExtendWith(SpringExtension.class)
@TestInstance(Lifecycle.PER_CLASS)
@Import({ MessageService.class, SecurityConf.class })
@ImportAutoConfiguration(AuthenticationFactoriesTestConf.class)
class MessageServiceUnitTest {
    @Autowired
    MessageService messageService;
    
    @MockBean
    JwtDecoder jwtDecoder;

    @Test
    void givenSecurityContextIsNotSet_whenGreet_thenThrowsAuthenticationCredentialsNotFoundException() {
        assertThrows(AuthenticationCredentialsNotFoundException.class, () -> messageService.getSecret());
    }

    @Test
    @WithAnonymousUser
    void givenUserIsAnonymous_whenGreet_thenThrowsAccessDeniedException() {
        assertThrows(AccessDeniedException.class, () -> messageService.getSecret());
    }

    @Test
    @WithJwt("ch4mpy.json")
    void givenUserIsCh4mpy_whenGreet_thenReturnGreetingWithPreferredUsernameAndAuthorities() {
        assertEquals("Hello ch4mpy! You are granted with [admin, ROLE_AUTHORIZED_PERSONNEL].", 
          messageService.greet());
    }

    @Test
    @WithMockUser(authorities = { "admin", "ROLE_AUTHORIZED_PERSONNEL" }, username = "ch4mpy")
    void givenSecurityContextIsPopulatedWithUsernamePasswordAuthenticationToken_whenGreet_thenThrowsClassCastException() {
        assertThrows(ClassCastException.class, () -> messageService.greet());
    }
}

一個 reactive 應用中 @Service 組件的單元測試與其它測試並沒有本質區別:

@ExtendWith(SpringExtension.class)
@TestInstance(Lifecycle.PER_CLASS)
@Import({ MessageService.class, SecurityConf.class })
@ImportAutoConfiguration(AuthenticationFactoriesTestConf.class)
class MessageServiceUnitTest {
    @Autowired
    MessageService messageService;
    
    @MockBean
    ReactiveJwtDecoder jwtDecoder;

    @Test
    void givenSecurityContextIsEmpty_whenGreet_thenThrowsAuthenticationCredentialsNotFoundException() {
        assertThrows(AuthenticationCredentialsNotFoundException.class, () -> messageService.greet()
            .block());
    }

    @Test
    @WithAnonymousUser
    void givenUserIsAnonymous_whenGreet_thenThrowsClassCastException() {
        assertThrows(ClassCastException.class, () -> messageService.greet()
            .block());
    }

    @Test
    @WithJwt("ch4mpy.json")
    void givenUserIsCh4mpy_whenGreet_thenReturnGreetingWithPreferredUsernameAndAuthorities() {
        assertEquals("Hello ch4mpy! You are granted with [admin, ROLE_AUTHORIZED_PERSONNEL].", 
          messageService.greet().block());
    }

    @Test
    @WithMockUser(authorities = { "admin", "ROLE_AUTHORIZED_PERSONNEL" }, username = "ch4mpy")
    void givenSecurityContextIsPopulatedWithUsernamePasswordAuthenticationToken_whenGreet_thenThrowsClassCastException() {
        assertThrows(ClassCastException.class, () -> messageService.greet().block());
    }
}

4.7. JUnit 5 <em @ParametrizedTest>

JUnit 5 允許定義可以在不同參數值下多次運行的測試。該參數可以是用於注入到安全上下文中模擬的 Authentication 對象。

<em @WithMockAuthentication 構建的認證實例獨立於 Spring 上下文,這使得它非常容易在參數化測試中使用:

@ParameterizedTest
@AuthenticationSource({
        @WithMockAuthentication(authorities = { "admin", "ROLE_AUTHORIZED_PERSONNEL" }, name = "ch4mpy"),
        @WithMockAuthentication(authorities = { "uncle", "PIRATE" }, name = "tonton-pirate") })
void givenUserIsAuthenticated_whenGetGreet_thenOk(@ParameterizedAuthentication Authentication auth) throws Exception {
    final var greeting = "Whatever the service returns";
    when(messageService.greet()).thenReturn(greeting);

    api.perform(get("/greet"))
        .andExpect(status().isOk())
        .andExpect(content().string(greeting));

    verify(messageService, times(1)).greet();
}

以下是翻譯後的內容:

上述代碼需要注意以下幾點:

  • 使用 @ParameterizedTest 代替 @Test
  • 使用 @AuthenticationSource 裝飾測試,該裝飾器包含所有 @WithMockAuthentication 的數組
  • 向測試方法添加 @ParameterizedAuthentication 參數

因為 @WithJwt 使用來自應用程序上下文的 Bean 來構建 Authentication 實例,所以我們需要做一些額外的事情:

@TestInstance(Lifecycle.PER_CLASS)
class MessageServiceUnitTest {

    @Autowired
    WithJwt.AuthenticationFactory authFactory;

    private Stream<AbstractAuthenticationToken> allIdentities() {
        final var authentications = authFactory.authenticationsFrom("ch4mpy.json", "tonton-pirate.json").toList();
        return authentications.stream();
    }

    @ParameterizedTest
    @MethodSource("allIdentities")
    void givenUserIsAuthenticated_whenGreet_thenReturnGreetingWithPreferredUsernameAndAuthorities(@ParameterizedAuthentication Authentication auth) {
        final var jwt = (JwtAuthenticationToken) auth;
        final var expected = "Hello %s! You are granted with %s.".formatted(jwt.getTokenAttributes().get(StandardClaimNames.PREFERRED_USERNAME), auth.getAuthorities());
        assertEquals(expected, messageService.greet());
    }
}

使用 @ParameterizedTest@WithJwt 時,我們的檢查清單如下:

  • 使用 @TestInstance(Lifecycle.PER_CLASS) 裝飾測試類
  • 自動注入 WithJwt.AuthenticationFactory
  • 定義一個返回認證流的方法,使用認證工廠進行每個認證
  • 使用 @ParameterizedTest 代替 @Test
  • 使用 @MethodSource 裝飾測試,引用上述定義的該方法
  • 在測試方法中添加 @ParameterizedAuthentication 參數

5. 使用模擬授權進行集成測試

我們將使用 Spring Boot 集成測試,並使用 @SpringBootTest,以便 Spring 將實際組件連接起來。為了繼續使用模擬身份,我們將使用它與 MockMvcWebTestClient 結合使用。這些測試本身以及填充測試安全上下文的選項,與單元測試相同。僅更改了測試設置:

  • 不再需要模擬組件或參數匹配器
  • 我們將使用 @SpringBootTest(webEnvironment = WebEnvironment.MOCK),而不是 @WebMvcTest@WebFluxTestMOCK 環境是與 MockMvcWebTestClient 結合使用模擬授權的最佳匹配
  • 明確裝飾測試類,使用 @AutoConfigureMockMvc@AutoConfigureWebTestClient,以便使用 MockMvcWebTestClient 進行注入

以下是 Spring Boot servlet 集成測試的骨架:

@SpringBootTest(webEnvironment = WebEnvironment.MOCK)
@AutoConfigureMockMvc
class ServletResourceServerApplicationTests {
    @Autowired
    MockMvc api;
    
    // Test structure and mocked identities options are the same as seen before in unit tests
}

這是在反應式應用中的等效實現:

@SpringBootTest(webEnvironment = WebEnvironment.MOCK)
@AutoConfigureWebTestClient
class ReactiveResourceServerApplicationTests {
    @Autowired
    WebTestClient api;
    
    // Test structure and mocked identities options are the same as seen before in unit tests
}

當然,這種集成測試可以保存模擬配置、參數捕獲等,但它也比單元測試慢得多,而且更加脆弱。我們應該謹慎使用它,可能覆蓋率低於 @WebMvcTest 或 @WebFluxTest,僅用於驗證自動裝配和跨組件通信是否正常工作。

6. 深入探索

我們目前測試了使用 JWT 解碼器保護的資源服務器,這些服務器在安全上下文中包含 JwtAuthenticationToken 實例。我們僅運行了帶有模擬 HTTP 請求的自動化測試,而沒有涉及任何授權服務器。

6.1. 使用任何類型的 OAuth2 身份驗證進行測試

如前所述,Spring OAuth2 安全上下文可以持有其他類型的 Authentication,因此在測試中,我們應該使用其他註解、請求後處理程序或轉換器

  • 默認情況下,具有  token 隱想的資源服務器具有 BearerTokenAuthentication 實例,測試應該使用 @WithOpaqueTokenopaqueToken()mockOpaqueToken()
  • 具有 oauth2Login() 的客户端通常具有 OAuth2AuthenticationToken 在其安全上下文中,我們使用 @WithOAuth2Login@WithOidcLoginoauth2Login()oidcLogin()mockOAuth2Login()mockOidcLogin()
  • 假設我們明確配置了自定義 Authentication 類型,例如使用 http.oauth2ResourceServer().jwt().jwtAuthenticationConverter(…) 或類似方法。在這種情況下,我們可能需要提供自己的單元測試工具,當使用 spring-addons 實現作為示例時,這並不複雜。同一 Github 倉庫還包含帶有自定義 Authentication 的樣本以及專用測試註解

6.2. 運行示例應用程序

提供的示例項目包含 Keycloak 實例的 master 領域(realm)的屬性,該實例運行在 https://localhost:8443。 使用任何其他 OIDC 授權服務器都需要僅調整 issuer-uri 屬性以及 Java 配置中的 authorities mapper,即將 realmRoles2AuthoritiesConverter Bean 更改為將新的授權服務器注入的角色映射從私有聲明(claim)中轉換。

對於 Keycloak 設置的更多詳細信息,請參閲 官方入門指南。 對於 獨立 zip 部署,可能更容易上手。

要設置本地使用 TLS 的 Keycloak 實例,並使用自簽名證書,此 GitHub 倉庫 將非常有用。

授權服務器應至少包含:

  • 兩個已聲明的用户,其中一個被授予 ROLE_AUTHORIZED_PERSONNEL 角色,而另一個未被授予
  • 一個已聲明的客户端,啓用了授權碼流程,以便像 Postman 這樣的工具可以代表這些用户獲取訪問令牌

7. 結論

在本文中,我們探討了使用模擬身份驗證 Spring OAuth2 訪問控制規則,在 Servlet 和 Reactive 應用程序中,使用 MockMvcWebTestClient 變體進行單元和集成測試的兩種選項:

  • 使用 spring-security-test 中的 MockMvc 請求後處理程序和 WebTestClient 變體
  • 使用 spring-addons-oauth2-test 中的 OAuth2 測試註解

我們還發現,可以使用 @ControllersMockMvc 請求後處理程序、WebTestClient 變體或註解進行測試。但是,只有後者能夠在我們測試其他類型的組件時設置安全上下文。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.