升级至Spring Boot 3.1.8(含Spring Security 6.x)和Java 21后OAuth 2.0 Resource Server JWT签名验证失败问题
我之前升级到Spring Boot 3.1.x + Java 21的时候也碰到过几乎一模一样的JWT签名验证问题,结合你给出的错误信息(客户端返回invalid_token,后端抛出BadJwtException: Signed JWT rejected: Invalid signature),大概率是以下几个常见原因导致的,咱们一步步来排查:
1. 密钥/证书不匹配或加载异常
这是最常见的原因,一定要先确认资源服务器用的验证密钥,和JWT签发方的签名密钥完全一致:
- 如果是非对称加密(如RSA):检查公钥格式是不是标准PEM格式?有没有多余的换行、空格或者缺失
-----BEGIN PUBLIC KEY-----/-----END PUBLIC KEY-----头尾部?另外Spring Security 6.x对密钥加载的细节有调整,Java 21的加密API对格式要求更严格,推荐显式加载公钥,比如:@Bean public JwtDecoder jwtDecoder() throws Exception { Resource publicKeyResource = new ClassPathResource("jwt-public.key"); byte[] publicKeyBytes = FileCopyUtils.copyToByteArray(publicKeyResource.getInputStream()); RSAPublicKey publicKey = (RSAPublicKey) KeyFactory.getInstance("RSA") .generatePublic(new X509EncodedKeySpec(publicKeyBytes)); return NimbusJwtDecoder.withPublicKey(publicKey).build(); } - 如果是对称加密(如HS256/HS512):要确保配置的密钥字符串和签发方完全一致,注意Java 21默认字符集可能和旧版本有差异,建议显式用UTF-8编码。另外HS256要求密钥至少256位(32个字符以上),短密钥在新版本里会直接被拒绝,比如配置文件可以这么写:
spring.security.oauth2.resourceserver.jwt.secret-key=your-256-bit-or-longer-secret-string-here
2. JWT算法不匹配
检查JWT的alg头字段和你资源服务器配置的算法是否完全对应:比如签发方用RS256签名,你资源服务器却配置成了HS256,肯定会验签失败。Spring Security 6.x对算法的校验比旧版本严格很多,不会自动兼容不匹配的算法。
你可以先用本地的JWT解析工具(比如离线的命令行工具或者离线解析器)把JWT解码,看alg字段的值是什么,再对应调整资源服务器的配置。比如如果是RS256,就一定要用公钥验签,不能用对称密钥。
3. Java 21加密提供者的变化
Java 21默认的安全加密提供者和旧版本(比如Java 17)有一些差异,部分小众加密算法的实现可能被移除或者默认不启用。如果你的JWT用了非标准算法,或者签发方用了比较特殊的加密方式,建议引入BouncyCastle作为第三方加密提供者:
- 先在依赖里加(Maven为例):
<dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcprov-jdk18on</artifactId> <version>1.77</version> </dependency> - 然后在启动类里注册提供者:
static { Security.addProvider(new BouncyCastleProvider()); }
4. Spring Security 6.x的自动配置变更
Spring Boot 3.1.x的JWT自动配置有一些细节调整:比如之前用spring.security.oauth2.resourceserver.jwt.jwk-set-uri配置JWK Set地址时,旧版本会自动匹配所有可用密钥,但新版本默认只会选第一个匹配算法的密钥,可能导致密钥不对应。
这种情况下建议自定义JwtDecoder,显式指定密钥选择策略,比如:
@Bean public JwtDecoder jwtDecoder(OAuth2ResourceServerProperties properties) throws Exception { String jwkSetUri = properties.getJwt().getJwkSetUri(); return NimbusJwtDecoder.withJwkSetUri(jwkSetUri) .jwtProcessorCustomizer(processor -> processor.setJWSKeySelector(JWSAlgorithmFamilyJWSKeySelector.fromJWKSetURL(new URL(jwkSetUri)))) .build(); }
排查小技巧
开启Spring Security的DEBUG日志,能看到JWT验签的每一步细节,比如加载了哪个密钥、用了什么算法,很容易定位问题:
logging.level.org.springframework.security.oauth2=DEBUG
备注:内容来源于stack exchange,提问作者Roma Kap

