Spring Boot Security资源服务器:JWT验证失败时如何刷新JWK集
解决方案
核心思路
当JWT因签名密钥不在本地JWK缓存导致验证失败时,主动触发JWK集刷新,再重新尝试验证当前JWT一次,避免直接返回401错误。
实现步骤
1. 自定义可手动刷新的JWK源
封装默认Nimbus JWK源,添加缓存和手动刷新逻辑,保证线程安全:
import com.nimbusds.jose.jwk.JWKSet import com.nimbusds.jose.jwk.source.JWKSource import com.nimbusds.jose.proc.SecurityContext import java.util.concurrent.locks.ReentrantLock class RefreshableJwkSource(private val delegate: JWKSource<SecurityContext>) : JWKSource<SecurityContext> { private val lock = ReentrantLock() private var cachedJwkSet: JWKSet? = null override fun getJWKs(context: SecurityContext): JWKSet { return cachedJwkSet ?: delegate.getJWKs(context).also { cachedJwkSet = it } } fun refresh() { lock.lock() try { cachedJwkSet = delegate.getJWKs(SecurityContext.NULL) } finally { lock.unlock() } } }
2. 构建带重试逻辑的JwtDecoder
捕获JWT签名/密钥相关验证异常,触发JWK刷新后重新验证:
import org.springframework.security.oauth2.jwt.Jwt import org.springframework.security.oauth2.jwt.JwtDecoder import org.springframework.security.oauth2.jwt.JwtValidationException class RetryOnRefreshJwtDecoder( private val decoderFactory: () -> JwtDecoder, private val refreshableJwkSource: RefreshableJwkSource ) : JwtDecoder { private var delegateDecoder: JwtDecoder = decoderFactory() override fun decode(token: String): Jwt { return try { delegateDecoder.decode(token) } catch (ex: JwtValidationException) { // 仅针对签名/密钥相关异常触发刷新,过滤过期、格式错误等无效请求 if (ex.message?.contains("signature") == true || ex.message?.contains("key") == true) { refreshableJwkSource.refresh() delegateDecoder = decoderFactory() delegateDecoder.decode(token) } else { throw ex } } } }
3. 替换Security配置中的默认JwtDecoder
将自定义组件注入Spring Security,替换默认实现:
import com.nimbusds.jose.jwk.source.JWKSource import com.nimbusds.jose.proc.SecurityContext import org.springframework.beans.factory.annotation.Value import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity import org.springframework.security.oauth2.jwt.JwtDecoder import org.springframework.security.oauth2.jwt.NimbusJwtDecoder import org.springframework.security.web.SecurityFilterChain @Configuration @EnableWebSecurity class MyCustomSecurityConfiguration { @Value("\${spring.security.oauth2.resourceserver.jwt.jwk-set-uri}") private lateinit var jwkSetUri: String @Bean fun refreshableJwkSource(): RefreshableJwkSource { val defaultJwkSource = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).jwkSource return RefreshableJwkSource(defaultJwkSource) } @Bean fun jwtDecoder(refreshableJwkSource: RefreshableJwkSource): JwtDecoder { val decoderFactory = { NimbusJwtDecoder.withJwkSetUri(jwkSetUri) .jwkSource(refreshableJwkSource) .build() } return RetryOnRefreshJwtDecoder(decoderFactory, refreshableJwkSource) } @Bean fun filterChain(http: HttpSecurity, jwtDecoder: JwtDecoder): SecurityFilterChain { http { authorizeRequests { authorize(anyRequest, authenticated) } oauth2ResourceServer { jwt { decoder = jwtDecoder } } } return http.build() } }
4. 保留原YAML配置
原配置无需修改,继续使用:
spring: security: oauth2: resourceserver: jwt: jwk-set-uri: https://idp.example.com/.well-known/jwks.json
关键说明
- 精准触发刷新:仅在签名/密钥验证失败时执行刷新,避免对过期、格式错误等无效请求做无意义重试。
- 线程安全保障:使用
ReentrantLock确保JWK刷新操作的原子性,防止并发刷新导致的资源冲突。 - Decoder重建:每次刷新JWK后重新构建
JwtDecoder,确保验证逻辑使用最新的密钥集。
内容的提问来源于stack exchange,提问作者scre_www
相关产品推荐
相关产品推荐

