如何正确测试JWT未授权场景?Spring Boot苹果登录鉴权测试疑难
问题分析与解决方案
核心问题原因
你自定义了authenticationManager,直接绕过了Spring Security OAuth2资源服务器默认的JWT校验流程。默认情况下,oauth2ResourceServer().jwt()会自动处理JWT的签名、issuer、audience、算法等校验逻辑,但自定义的authenticationManager直接调用tokenService.parseToken,且parseToken用try-catch吞掉了所有异常返回null,再手动抛出InvalidBearerTokenException。同时测试中使用的Spring Security Test的jwt()方法是直接模拟已认证的Authentication对象,不会走真实的JWT解码校验流程,导致无效JWT测试也返回200。
解决步骤
1. 修复安全配置,复用框架默认JWT校验逻辑
移除自定义的authenticationManager,让Spring Security自动处理JWT校验,仅通过JwtAuthenticationConverter做必要的权限或用户信息转换:
@Configuration class OAuth2ResourceServerSecurityConfig { @Bean fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { http .csrf { it.ignoringRequestMatchers("/api/**") } .authorizeHttpRequests { it.requestMatchers("/api/token/apple/code").authenticated() it.anyRequest().permitAll() } .oauth2ResourceServer { it.jwt { jwt -> jwt.jwtAuthenticationConverter(jwtAuthenticationConverter()) } } return http.build() } @Bean fun jwtAuthenticationConverter(): JwtAuthenticationConverter { val converter = JwtAuthenticationConverter() // 自定义权限转换(如果需要) converter.setJwtGrantedAuthoritiesConverter { _ -> listOf(SimpleGrantedAuthority("USER")) } return converter } }
2. 调整服务与控制器逻辑
不再在tokenService中处理JWT解码,直接利用框架解析后的Jwt对象:
// DefaultTokenService.kt @Service class DefaultTokenService : TokenService { override fun convertJwtToAppleUser(jwt: Jwt): AppleUser { val userId = jwt.claims["sub"] as String val email = jwt.claims["email"] as String return DefaultAppleUser(userId, email) } // 其他实现... }
// TokenController.kt @RestController @RequestMapping("/api/token") class TokenController( private val tokenService: TokenService ) { @GetMapping("/apple/code") fun getRefreshTokenOfApple( @AuthenticationPrincipal jwt: Jwt, @RequestParam authorizationCode: String, ): AppleTokenResponse? { val appleUser = tokenService.convertJwtToAppleUser(jwt) return tokenService.getRefreshToken(authorizationCode) } // 其他实现... }
3. 编写真实的JWT校验测试用例
使用Nimbus Jose JWT库生成符合测试配置的有效/无效JWT,模拟真实请求头发送测试,确保校验逻辑生效:
@SpringBootTest @AutoConfigureMockMvc class TokenControllerTest{ @Autowired private lateinit var mockMvc: MockMvc @SpykBean private lateinit var spyStubTokenService: DefaultTokenService // 生成测试用RSA密钥对 private val keyPair = KeyPairGenerator.getInstance("RSA").apply { initialize(2048) }.generateKeyPair() private val signer = JWSSignerFactory.getInstance().createJWSSigner(JWSAlgorithm.RS256, keyPair.private) // 替换测试环境的JwtDecoder,使用本地密钥验证 @Bean fun testJwtDecoder(): JwtDecoder { val jwk = RSAKey.Builder(keyPair.public as RSAPublicKey).keyID("test-key").build() val jwkSource = ImmutableJWKSet(JWKSet(jwk)) val decoder = NimbusJwtDecoder.withJwkSetUri("https://expected.com/keys").jwkSource(jwkSource).build() // 配置与测试application.yml一致的校验规则 val validator = JwtValidators.createDefaultWithIssuer("https://expected.com") validator.withAudience("expected audiences") validator.withJwsAlgorithms(Sets.newHashSet(JWSAlgorithm.RS256)) decoder.setJwtValidator(validator) return decoder } // 生成符合要求的有效JWT private fun generateValidJwt(): String { val claims = JWTClaimsSet.Builder() .issuer("https://expected.com") .audience("expected audiences") .subject("test-user-id") .claim("email", "test@example.com") .expirationTime(Date.from(Instant.now().plusHours(1))) .build() val jwsObject = JWSObject(JWSHeader(JWSAlgorithm.RS256), Payload(claims.toJSONObject())) jwsObject.sign(signer) return jwsObject.serialize() } // 生成issuer无效的JWT private fun generateInvalidIssuerJwt(): String { val claims = JWTClaimsSet.Builder() .issuer("https://invalid.com") .audience("expected audiences") .subject("test-user-id") .claim("email", "test@example.com") .expirationTime(Date.from(Instant.now().plusHours(1))) .build() val jwsObject = JWSObject(JWSHeader(JWSAlgorithm.RS256), Payload(claims.toJSONObject())) jwsObject.sign(signer) return jwsObject.serialize() } // 生成算法无效的JWT private fun generateInvalidAlgorithmJwt(): String { val hmacKey = SecretKeySpec("test-secret".toByteArray(), "HmacSHA256") val hmacSigner = JWSSignerFactory.getInstance().createJWSSigner(JWSAlgorithm.HS256, hmacKey) val claims = JWTClaimsSet.Builder() .issuer("https://expected.com") .audience("expected audiences") .subject("test-user-id") .claim("email", "test@example.com") .expirationTime(Date.from(Instant.now().plusHours(1))) .build() val jwsObject = JWSObject(JWSHeader(JWSAlgorithm.HS256), Payload(claims.toJSONObject())) jwsObject.sign(hmacSigner) return jwsObject.serialize() } @Nested inner class GetRefreshTokenOfApple { @Test fun `当JWT issuer无效时返回401`() { val result = mockMvc.perform( MockMvcRequestBuilders .get("/api/token/apple/code") .param("authorizationCode", "authorization code 1") .header("Authorization", "Bearer ${generateInvalidIssuerJwt()}") ) result.andExpect(MockMvcResultMatchers.status().isUnauthorized) } @Test fun `当JWT算法无效时返回401`() { val result = mockMvc.perform( MockMvcRequestBuilders .get("/api/token/apple/code") .param("authorizationCode", "authorization code 1") .header("Authorization", "Bearer ${generateInvalidAlgorithmJwt()}") ) result.andExpect(MockMvcResultMatchers.status().isUnauthorized) } @Test fun `当JWT有效时返回200并调用tokenService`() { val result = mockMvc.perform( MockMvcRequestBuilders .get("/api/token/apple/code") .param("authorizationCode", "authorization code 1") .header("Authorization", "Bearer ${generateValidJwt()}") ) result.andExpect(MockMvcResultMatchers.status().isOk) verify { spyStubTokenService.getRefreshToken("authorization code 1") } } } }
测试必要性说明
这类测试非常必要:
- JWT的issuer、audience、算法、签名校验是接口安全的核心防线,一旦失效会导致恶意构造的JWT绕过认证,引发数据泄露或权限越权风险。
- 通过测试可以验证配置的校验规则是否正确生效,避免因配置错误、代码逻辑疏漏导致的安全漏洞。
内容的提问来源于stack exchange,提问作者Yuta Shoji
相关产品推荐
相关产品推荐

