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, Mockito 和 JUnit 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 應用程序編寫單元測試,而無需依賴外部令牌生成或驗證服務。這種方法確保我們的測試快速、可靠且獨立於外部依賴。