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

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字符串

排查点(若问题仍存在)

  1. 检查所有实例的私钥PEM是否完全一致(无换行、空格错误)
  2. 查看资源服务器的验证日志,确认是签名验证失败还是kid不匹配
  3. 访问各实例的.well-known/jwks.json端点,对比返回的密钥内容是否完全相同

内容的提问来源于stack exchange,提问作者Pranjal Sharma

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.19 06:47:09