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,可通过以下自定义扩展实现:
- 自定义Token请求处理器:继承
OAuth2TokenEndpointFilter或实现OAuth2TokenGenerator,生成令牌时从请求体/请求头提取用户信息(如订阅ID),添加到JWT的Claims中。 - 扩展客户端凭证授权转换器:实现
OAuth2ClientCredentialsAuthenticationConverter,转换认证请求时从自定义请求参数(如subscription_id)或Header中获取用户信息,封装到OAuth2ClientCredentialsAuthenticationToken的附加属性里,后续生成令牌时读取这些属性。
注意:这种方式偏离了标准客户端凭证流的设计(该流原本用于客户端自身认证,而非用户认证),若非必须使用Authorization Server,不建议这么做。
针对疑问c
Spring OAuth2 Authorization Server支持无停机公钥轮换,正确步骤如下:
- 配置多密钥对:不要仅配置单个密钥,而是配置密钥集合——比如用
KeyStoreKeyManager加载多个密钥条目,或用InMemoryKeyManager添加多组RSA密钥对。 - 设置密钥轮换策略:自定义
OAuth2TokenGenerator,指定新令牌使用新密钥签名,同时保留旧密钥用于验证未过期的旧令牌。 - 无需重启或调用refresh接口:服务器同时加载新旧密钥后,JWKS端点会自动返回所有有效密钥(包括旧密钥,直到其对应的令牌全部过期),合作方会自动从JWKS获取所有公钥,验证时尝试匹配正确的公钥。
注意:不能直接替换单个密钥文件,否则会导致旧令牌无法验证,必须保持新旧密钥同时生效,直到旧令牌有效期结束。
针对疑问d(仅用Spring Security生成JWT)
步骤如下:
- 添加依赖:引入
spring-security-oauth2-jose依赖,它包含JWT生成和解析的核心类。 - 配置密钥对:生产环境建议用
keytool生成JKS文件存储RSA密钥对;测试环境可在代码中临时生成。 - 编写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(); } }
- 业务代码调用:用户认证通过后,调用工具类生成JWT,再重定向到合作方URL并携带令牌(可作为查询参数或Cookie,根据合作方要求)。
针对疑问e(仅用Spring Security实现JWKS端点)
实现非常简便,步骤如下:
- 编写JWKS端点Controller:返回符合JWKS格式的公钥信息,标准路径建议用
/.well-known/jwks.json,方便合作方自动识别。 - 示例代码:
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; } }
- 密钥轮换支持:需要轮换密钥时,只需在
JwkSet中添加新的公钥,同时保留旧公钥直到对应令牌过期即可,实现无停机轮换。
内容的提问来源于stack exchange,提问作者Mark
相关产品推荐
相关产品推荐

