使用JwtDecoder模擬JWT在JUnit測試中

Spring Security,Testing
Remote
1
11:50 AM · Nov 30 ,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 相關的依賴項支持 JWT 在 Spring Security 中的使用,包括 JwtDecoder 接口,用於解碼和驗證 JWT。 依賴項 mockito-core 允許我們在測試中模擬依賴項,從而確保我們能夠隔離單元測試中的 UserController,  從外部系統。

2.2. 創建 UserController

接下來,我們將創建一個 UserController,具有 /user 端點,用於根據 JWT 令牌檢索用户信息。 它驗證令牌,檢查其過期時間,並提取用户的 subject:

@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")));
}

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

4. 其他場景測試

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

4.1. 測試無效令牌

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

@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

測試斷言一個 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.