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

Spring Boot生成自定义JWT及JWKS端点相关技术咨询

需求

我方应用与合作方需通过JWT令牌通信,流程如下:

  • 用户已通过其他认证方案在MyApp完成认证;
  • MyApp生成包含订阅ID等用户信息的JWT令牌;
  • MyApp携带JWT令牌重定向至合作方URL;
  • 合作方通过我方暴露的JWKS端点获取公钥,验证JWT由我方签发。
主要疑问
  • a. 是否需要使用Spring OAuth2 Authorization Server,还是仅用Spring Security组件即可生成令牌并暴露JWKS端点?
  • b. 若使用Spring OAuth2 Authorization Server,如何自定义/oauth2/token接口以接收用户信息来生成令牌?
  • c. 若使用Spring OAuth2 Authorization Server,如何实现公钥轮换?
  • d. 若仅使用Spring Security组件,能否提供生成JWT的教程?
  • e. 若仅使用Spring Security组件,实现JWKS端点是否简便?
补充疑问
  • 关于疑问a:无需客户端凭证流这类授权服务器,我方应用可自行生成JWT,考虑到Spring Authorization Server具备完整的令牌生成架构,是否有必要采用它,还是仅用Spring Security组件更简便?
  • 关于疑问b:若决定使用Spring OAuth2 Authorization Server,计划采用客户端凭证调用/oauth2/token,但请求中无用户信息,如何生成包含特定用户信息的JWT?能否自定义请求在请求体或头部传递所需信息?
  • 关于疑问c:在Spring OAuth2 Authorization Server中能否实现无停机公钥轮换?是否只需生成新JKS文件、修改服务器指向路径并调用类似POST /refresh接口即可?

解决方案

针对疑问a(含补充)

优先选择仅用Spring Security组件。你的场景不需要完整的OAuth2授权服务器能力(如客户端管理、多授权流支持),Spring Security的JWT模块已完全满足需求,实现更轻量、灵活,能减少不必要的依赖和配置复杂度。Spring Authorization Server适合对外提供标准化OAuth2服务的场景(比如给第三方客户端开放多种授权方式),你的场景完全可以自行生成JWT,没必要引入整套授权服务器。

针对疑问b

如果一定要用Spring OAuth2 Authorization Server,客户端凭证流本身无用户上下文,要生成带用户信息的JWT,可通过以下自定义扩展实现:

  1. 自定义Token请求处理器:继承OAuth2TokenEndpointFilter或实现OAuth2TokenGenerator,生成令牌时从请求体/请求头提取用户信息(如订阅ID),添加到JWT的Claims中。
  2. 扩展客户端凭证授权转换器:实现OAuth2ClientCredentialsAuthenticationConverter,转换认证请求时从自定义请求参数(如subscription_id)或Header中获取用户信息,封装到OAuth2ClientCredentialsAuthenticationToken的附加属性里,后续生成令牌时读取这些属性。

注意:这种方式偏离了标准客户端凭证流的设计(该流原本用于客户端自身认证,而非用户认证),若非必须使用Authorization Server,不建议这么做。

针对疑问c

Spring OAuth2 Authorization Server支持无停机公钥轮换,正确步骤如下:

  1. 配置多密钥对:不要仅配置单个密钥,而是配置密钥集合——比如用KeyStoreKeyManager加载多个密钥条目,或用InMemoryKeyManager添加多组RSA密钥对。
  2. 设置密钥轮换策略:自定义OAuth2TokenGenerator,指定新令牌使用新密钥签名,同时保留旧密钥用于验证未过期的旧令牌。
  3. 无需重启或调用refresh接口:服务器同时加载新旧密钥后,JWKS端点会自动返回所有有效密钥(包括旧密钥,直到其对应的令牌全部过期),合作方会自动从JWKS获取所有公钥,验证时尝试匹配正确的公钥。

注意:不能直接替换单个密钥文件,否则会导致旧令牌无法验证,必须保持新旧密钥同时生效,直到旧令牌有效期结束。

针对疑问d(仅用Spring Security生成JWT)

步骤如下:

  1. 添加依赖:引入spring-security-oauth2-jose依赖,它包含JWT生成和解析的核心类。
  2. 配置密钥对:生产环境建议用keytool生成JKS文件存储RSA密钥对;测试环境可在代码中临时生成。
  3. 编写JWT生成工具类:
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
import org.springframework.security.oauth2.jose.jws.RsaSigner;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.time.Instant;
import java.time.temporal.ChronoUnit;

public class JwtGenerator {
    private final JwtEncoder jwtEncoder;

    public JwtGenerator() throws Exception {
        // 生产环境建议从密钥库加载,此处为示例生成临时密钥对
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
        keyPairGenerator.initialize(2048);
        KeyPair keyPair = keyPairGenerator.generateKeyPair();
        RsaSigner signer = new RsaSigner(keyPair.getPrivate());
        this.jwtEncoder = new NimbusJwtEncoder(signer);
    }

    public String generateToken(String userId, String subscriptionId) {
        Instant now = Instant.now();
        JwtClaimsSet claims = JwtClaimsSet.builder()
                .issuer("myapp") // 我方应用标识,合作方验证时会检查
                .subject(userId)
                .claim("subscription_id", subscriptionId) // 自定义用户信息
                .issuedAt(now)
                .expiresAt(now.plus(15, ChronoUnit.MINUTES)) // 设置令牌有效期
                .build();
        return this.jwtEncoder.encode(JwtEncoderParameters.from(claims)).getTokenValue();
    }
}
  1. 业务代码调用:用户认证通过后,调用工具类生成JWT,再重定向到合作方URL并携带令牌(可作为查询参数或Cookie,根据合作方要求)。

针对疑问e(仅用Spring Security实现JWKS端点)

实现非常简便,步骤如下:

  1. 编写JWKS端点Controller:返回符合JWKS格式的公钥信息,标准路径建议用/.well-known/jwks.json,方便合作方自动识别。
  2. 示例代码:
import org.springframework.security.oauth2.jwt.JwkSet;
import org.springframework.security.oauth2.jose.jwk.RsaJwk;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.util.List;

@RestController
public class JwksController {
    private final JwkSet jwkSet;

    public JwksController() throws Exception {
        // 生产环境建议从密钥库加载,此处为示例生成临时密钥对
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
        keyPairGenerator.initialize(2048);
        KeyPair keyPair = keyPairGenerator.generateKeyPair();
        RsaJwk jwk = RsaJwk.builder()
                .kid("myapp-key-1") // 密钥ID,用于标识不同密钥
                .publicKey(keyPair.getPublic())
                .build();
        this.jwkSet = new JwkSet(List.of(jwk));
    }

    @GetMapping("/.well-known/jwks.json")
    public JwkSet getJwks() {
        return this.jwkSet;
    }
}
  1. 密钥轮换支持:需要轮换密钥时,只需在JwkSet中添加新的公钥,同时保留旧公钥直到对应令牌过期即可,实现无停机轮换。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.12 07:40:32