You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何正确测试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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.03 20:00:58