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