知識庫 / Testing RSS 訂閱

使用 JwtDecoder 模擬 JWT 在 JUnit 測試中

Spring Security,Testing
HongKong
5
10:51 AM · Dec 06 ,2025

1. 概述

在本教程中,我們將探討如何有效地模擬 JWT(JSON Web Token)以進行單元測試,Spring Security應用程序,這些應用程序使用JWT身份驗證。 測試JWT安全端點通常需要模擬不同的JWT場景,而無需依賴實際的令牌生成或驗證。 這種方法允許我們編寫健壯的單元測試,而無需在測試期間管理真實的JWT令牌的複雜性。

模擬JWT解碼在單元測試中非常重要,因為它允許我們隔離身份驗證邏輯與外部依賴項,例如令牌生成服務或第三方身份提供商。 通過模擬不同的JWT場景,我們可以確保我們的應用程序正確處理有效的令牌、自定義聲明、無效令牌和到期令牌。

我們將學習如何使用 Mockito 模擬 JwtDecoder,創建自定義JWT聲明,並測試各種場景。 在本教程結束時,我們將能夠為Spring Security基於JWT的身份驗證邏輯編寫全面的單元測試。

2. 環境搭建與配置

在開始編寫測試用例之前,讓我們先設置好測試環境並安裝必要的依賴項。

2.1. 依賴項

我們將使用 Spring Security OAuth2、MockitoJUnit 5 用於我們的測試:

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-oauth2-jose</artifactId>
    <version>6.4.2</version>
</dependency>
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>5.15.2</version>
    <scope>test</scope>
</dependency>

spring-security-oauth2-jose 依賴項支持 Spring Security 中的 JWT,包括 JwtDecoder 接口,該接口用於解碼和驗證 JWT。 mockito-core 依賴項允許我們在測試中模擬依賴項,從而確保我們能夠隔離單元測試中的 UserController,  來自外部系統。

2.2. 創建 UserController

接下來,我們將創建 UserController,並使用 @GetMapping(“/user”)端點來根據 JWT 令牌檢索用户信息。它會驗證令牌,檢查其是否已過期,並提取用户的主體:

@GetMapping("/user")
public ResponseEntity<String> getUserInfo(@AuthenticationPrincipal Jwt jwt) {
    if (jwt == null || jwt.getSubject() == null) {
        throw new JwtValidationException("Invalid token", Arrays.asList(new OAuth2Error("invalid_token")));
    }

    Instant expiration = jwt.getExpiresAt();
    if (expiration != null && expiration.isBefore(Instant.now())) {
        throw new JwtValidationException("Token has expired", Arrays.asList(new OAuth2Error("expired_token")));
    }

    return ResponseEntity.ok("Hello, " + jwt.getSubject());
}

2.3. 設置測試類

讓我們創建一個測試類 MockJwtDecoderJUnitTest,並使用 Mockito 來模擬 JwtDecoder。以下是初始設置:

@ExtendWith(MockitoExtension.class)
public class MockJwtDecoderJUnitTest {
    @Mock
    private JwtDecoder jwtDecoder;

    @InjectMocks
    private UserController userController;

    @BeforeEach
    void setUp() {
        SecurityContextHolder.clearContext();
    }
}

在本次配置中,我們使用 @ExtendWith(MockitoExtension.class)  啓用 Mockito 在我們的 JUnit 測試中。 JwtDecoder  使用 @Mock,  被模擬,並且 UserController  使用 @InjectMocks  被注入包含模擬的 JwtDecoder SecurityContextHolder  在每次測試前被清除,以確保測試環境的乾淨狀態。

3. 模擬 JWT 解碼

在環境配置完成後,我們編寫測試用例來模擬 JWT 解碼。我們首先測試一個有效的 JWT 令牌。

3.1. 驗證有效令牌

應用程序應在提供有效令牌時返回用户信息。以下是如何測試此場景的方法:

@Test
void whenValidToken_thenReturnsUserInfo() {
    Map<String, Object> claims = new HashMap<>();
    claims.put("sub", "john.doe");
    
    Jwt jwt = Jwt.withTokenValue("token")
      .header("alg", "none")
      .claims(existingClaims -> existingClaims.putAll(claims))
      .build();

    JwtAuthenticationToken authentication = new JwtAuthenticationToken(jwt);
    SecurityContextHolder.getContext().setAuthentication(authentication);

    ResponseEntity<String> response = userController.getUserInfo(jwt);
    
    assertEquals("Hello, john.doe", response.getBody());
    assertEquals(HttpStatus.OK, response.getStatusCode());
}

在本測試中,我們創建一個模擬的 JWT,其中包含一個 sub (主題)聲明。 JwtAuthenticationToken  用於設置安全上下文,而 UserController  處理該令牌並返回響應。 我們使用斷言驗證響應。

3.2. 自定義聲明的測試

有時,JWT 包含自定義聲明,例如角色或電子郵件地址。例如,如果 UserController 使用 roles 聲明來授權訪問,則測試應檢查控制器是否根據聲明的角色按預期運行:

@Test
void whenTokenHasCustomClaims_thenProcessesCorrectly() {
    Map<String, Object> claims = new HashMap<>();
    claims.put("sub", "john.doe");
    claims.put("roles", Arrays.asList("ROLE_USER", "ROLE_ADMIN"));
    claims.put("email", "[email protected]");

    Jwt jwt = Jwt.withTokenValue("token")
      .header("alg", "none")
      .claims(existingClaims -> existingClaims.putAll(claims))
      .build();

    List authorities = ((List) jwt.getClaim("roles"))
      .stream()
      .map(role -> new SimpleGrantedAuthority(role))
      .collect(Collectors.toList());

    JwtAuthenticationToken authentication = new JwtAuthenticationToken(
      jwt,
      authorities,
      jwt.getClaim("sub")
    );

    SecurityContextHolder.getContext().setAuthentication(authentication);

    ResponseEntity response = userController.getUserInfo(jwt);

    assertEquals("Hello, john.doe", response.getBody());
    assertEquals(HttpStatus.OK, response.getStatusCode());

    assertTrue(authentication.getAuthorities().stream()
      .anyMatch(auth -> auth.getAuthority().equals("ROLE_ADMIN")));
}

在本測試中,我們驗證 角色 聲明是否正確處理,並且用户擁有預期的權限(在本例中為 ROLE_ADMIN)。

4. 測試其他場景

接下來,我們將探索不同的測試用例。

4.1. 測試無效令牌

當提供無效令牌時,應用程序應拋出 <em >JwtValidationException</em> 異常。 讓我們編寫一個快速測試,以驗證 <em >JwtDecoder</em> 在嘗試解碼無效令牌時是否正確地拋出異常:

@Test
void whenInvalidToken_thenThrowsException() {
    Map<String, Object> claims = new HashMap<>();
    claims.put("sub", null);

    Jwt invalidJwt = Jwt.withTokenValue("invalid_token")
      .header("alg", "none")
      .claims(existingClaims -> existingClaims.putAll(claims))
      .build();

    JwtAuthenticationToken authentication = new JwtAuthenticationToken(invalidJwt);
    SecurityContextHolder.getContext()
      .setAuthentication(authentication);

    JwtValidationException exception = assertThrows(JwtValidationException.class, () -> {
      userController.getUserInfo(invalidJwt);
    });

    assertEquals("Invalid token", exception.getMessage());
}

在本次測試中,我們模擬 JwtDecoder 在處理 null 令牌時拋出 JwtValidationException

測試斷言表明,當拋出異常時,異常信息為 “Invalid token“。

4.2. 過期令牌的測試

當提供一個已過期的令牌時,應用程序應拋出 JwtValidationException 異常。 以下測試驗證 JwtDecoder 在嘗試解碼已過期令牌時是否正確地拋出異常:

@Test
void whenExpiredToken_thenThrowsException() throws Exception {
    Map<String, Object> claims = new HashMap<>();
    claims.put("sub", "john.doe");
    claims.put("exp", Instant.now().minus(1, ChronoUnit.DAYS));

    Jwt expiredJwt = Jwt.withTokenValue("expired_token")
      .header("alg", "none")
      .claims(existingClaims -> existingClaims.putAll(claims))
      .build();

    JwtAuthenticationToken authentication = new JwtAuthenticationToken(expiredJwt);
    SecurityContextHolder.getContext()
      .setAuthentication(authentication);
    JwtValidationException exception = assertThrows(JwtValidationException.class, () -> {
      userController.getUserInfo(expiredJwt);
    });

    assertEquals("Token has expired", exception.getMessage());
}

在本測試中,我們設置了過期時間為1天前,以模擬過期令牌。

該測試斷言當拋出 JwtValidationException 異常,且消息為 "Token has expired“。

5. 結論

在本教程中,我們學習瞭如何使用 Mockito 在 JUnit 測試中模擬 JWT 解碼。我們涵蓋了各種場景,包括測試帶有自定義聲明的有效令牌、處理無效令牌和管理已過期的令牌。

通過模擬 JWT 解碼,我們可以編寫 Spring Security 應用程序的單元測試,而無需依賴外部令牌生成或驗證服務。這種方法確保我們的測試快速、可靠且獨立於外部依賴。

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

發佈 評論

Some HTML is okay.