Spring OAuth2认证服务器水平扩展引发认证失败求助
解决方案:Spring OAuth2认证服务器多实例令牌一致性问题
核心问题分析
你的问题根源在于多实例未共享同一套密钥对及JWK配置:即使硬编码了公钥,若私钥未同步、JWK的kid(密钥ID)随机生成,会导致不同实例签发的令牌签名无法被其他实例验证,或JWKS端点返回的密钥不一致,最终引发认证失败。
步骤1:统一密钥对加载(公钥+私钥)
你仅实现了公钥的硬编码,必须确保所有实例加载完全相同的RSA公钥和私钥。补充私钥加载逻辑:
private RSAPublicKey publicKey() { String publicKeyPem = "-----BEGIN PUBLIC KEY-----\r\n" + "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmbUIg34wINnYnjAkdmLj\r\n" + "4HtVpM7H0/T4aPAdoLLt9aNNgaBbQ0qS6iEtt1VHiua87Spp3DQpJ0Nyo65hYVgc\r\n" + "5KpalBbhH+Bz0Mj9TR/24XGV3VOfmy0pIRZJ4EJZS8WP7+lH0XUyhzVmAEYwaWok\r\n" + "LLpmmXHRxXk02KkEtgObL0TyEko3ygxf6O0d0+XuX9GchzBLMR+lt4gOoOld1w2L\r\n" + "7CJ9UmF1diesr979xs+nMcST1VkWvR3QBg2cchwZEe5VR/+FD7US1s/mPNxumAgi\r\n" + "uJzyLxLNeaCjWueZ4zdj4586YNxFOrLg6j6Ud3bpoX37rnNWWgnuZjNejS6MB5Ov\r\n" + "4QIDAQAB\r\n" + "-----END PUBLIC KEY-----"; try { String publicKeyPemContent = publicKeyPem.replaceAll("-----BEGIN PUBLIC KEY-----", "") .replaceAll("-----END PUBLIC KEY-----", "") .replaceAll("\\s", ""); byte[] publicKeyBytes = Base64.getDecoder().decode(publicKeyPemContent); X509EncodedKeySpec spec = new X509EncodedKeySpec(publicKeyBytes); KeyFactory keyFactory = KeyFactory.getInstance("RSA"); return (RSAPublicKey) keyFactory.generatePublic(spec); } catch (Exception e) { throw new RuntimeException("Failed to parse public key", e); } } private RSAPrivateKey privateKey() { // 替换为你的私钥PEM内容 String privateKeyPem = "-----BEGIN PRIVATE KEY-----\r\n" + "YOUR_FULL_PRIVATE_KEY_PEM_HERE\r\n" + "-----END PRIVATE KEY-----"; try { String privateKeyPemContent = privateKeyPem.replaceAll("-----BEGIN PRIVATE KEY-----", "") .replaceAll("-----END PRIVATE KEY-----", "") .replaceAll("\\s", ""); byte[] privateKeyBytes = Base64.getDecoder().decode(privateKeyPemContent); PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(privateKeyBytes); KeyFactory keyFactory = KeyFactory.getInstance("RSA"); return (RSAPrivateKey) keyFactory.generatePrivate(spec); } catch (Exception e) { throw new RuntimeException("Failed to parse private key", e); } }
步骤2:固定JWK的Key ID(kid)
删除UUID.randomUUID().toString(),使用固定的kid值,确保所有实例的JWKS端点返回相同的密钥标识:
public JWKSource<SecurityContext> jwkSource() { RSAPublicKey publicKey = publicKey(); RSAPrivateKey privateKey = privateKey(); RSAKey rsaKey = new RSAKey.Builder(publicKey) .privateKey(privateKey) .keyID("fixed-rsa-oauth2-key") // 固定值,所有实例一致 .issueTime(null) .expirationTime(null) .build(); JWKSet jwkSet = new JWKSet(rsaKey); return new ImmutableJWKSet<>(jwkSet); }
步骤3:验证配置一致性
- 确保所有实例的令牌签名算法统一(如默认的
RS256) - 资源服务器(主应用)验证令牌时,若通过JWKS端点获取公钥,需确保指向认证服务器的负载均衡地址(而非单个实例);若硬编码公钥,必须与认证服务器的公钥完全一致
- 无需硬编码
iat(签发时间):JWT验证允许时间存在合理偏差(默认±60秒),毫秒级差异不影响认证
进阶优化(避免硬编码)
在PCF环境中,推荐使用CredHub存储密钥对,通过Spring Boot环境变量注入加载,避免代码硬编码的安全风险:
@Value("${oauth2.rsa.public-key}") private String publicKeyPem; @Value("${oauth2.rsa.private-key}") private String privateKeyPem; // 修改publicKey()和privateKey()方法,使用注入的PEM字符串
排查点(若问题仍存在)
- 检查所有实例的私钥PEM是否完全一致(无换行、空格错误)
- 查看资源服务器的验证日志,确认是签名验证失败还是
kid不匹配 - 访问各实例的
.well-known/jwks.json端点,对比返回的密钥内容是否完全相同
内容的提问来源于stack exchange,提问作者Pranjal Sharma
相关产品推荐
相关产品推荐

