JWT(JSON Web
Token)是目前互联网主流的无状态身份认证方案 ,广泛应用于单体项目、微服务、SSO
单点登录、OAuth2.0/OIDC 第三方授权场景。相较于传统 Session 认证,JWT
无需服务端存储会话信息,依靠签名校验实现身份鉴权,配合 JWKS
公钥地址可实现密钥无感轮换,完美适配分布式系统架构。
本文将从 JWT 基础结构、完整认证流程、JWKS 公钥网址核心作用、带/不带
Kid 的 Token 差异、Node.js 与 SpringBoot 实战验签代码全方位总结 JWT
核心知识。
一、JWT 三段结构 Mermaid
示意图
标准 JWT 由
Header(头部)、Payload(载荷)、Signature(签名)
三部分组成,通过 . 拼接为完整令牌,前两段为 Base64Url
编码明文,最后一段为加密签名,不可篡改。
graph LR
A[JWT完整令牌Header.Payload.Signature] --> B[Header 头部]
A --> C[Payload 载荷]
A --> D[Signature 签名]
B --> B1[加密算法:RS256/HS256]
B --> B2[可选字段:kid 密钥唯一标识]
C --> C1[标准声明:exp过期时间/iat签发时间]
C --> C2[自定义声明:用户ID/角色/权限]
D --> D1[私钥加密生成公钥全局验签]
1.1 带 Kid / 不带 Kid 的 JWT
结构详解
1)不带 Kid 的
JWT(单体应用专用)
适用于系统仅存在唯一一对公私钥 、无需密钥轮换的单体项目,服务端直接使用本地固定公钥验签,无需
JWKS 公钥地址。
Header 解码内容
1 2 3 4 { "alg" : "RS256" , "typ" : "JWT" }
Payload 解码内容
1 2 3 4 5 6 7 { "sub" : "10001" , "name" : "张三" , "role" : "admin" , "iat" : 1715000000 , "exp" : 1715864000 }
2)带 Kid 的 JWT(微服务/JWKS
标准)
Kid(密钥唯一标识) 是适配 JWKS
多密钥、密钥平滑轮换的核心字段,微服务、SSO、第三方授权场景必须携带。服务端可根据
Token 头部的 kid,在 JWKS 公钥集合中精准匹配对应公钥完成验签。
Header 解码内容
1 2 3 4 5 { "alg" : "RS256" , "typ" : "JWT" , "kid" : "key-2026-05" }
Payload 载荷内容与无 Kid 模式一致,仅头部新增密钥标识字段。
二、Mermaid 时序图(JWT
基础认证流程)
基础 JWT 认证流程适用于单体应用、无
JWKS、固定公私钥模式,核心逻辑:私钥签发令牌、公钥本地验签、无状态鉴权 。
sequenceDiagram
participant 客户端
participant 认证服务端
participant 资源接口服务
客户端->>认证服务端: 1.提交账号密码发起登录
认证服务端-->>认证服务端: 2.校验账号密码合法性
认证服务端->>客户端: 3.私钥签名生成JWT并返回
客户端->>资源接口服务: 4.携带JWT请求业务接口(Header:Bearer)
资源接口服务-->>资源接口服务: 5.本地固定公钥验签+校验过期时间
alt 校验通过
资源接口服务-->>客户端: 6.返回业务数据
else 校验失败/令牌过期
资源接口服务-->>客户端: 7.返回401未授权
end
三、带 JWKS
公钥网址的完整时序流程
JWKS(JSON Web Key Set)是分布式系统标准方案,通过公开公钥地址
https://xxx.com/.well-known/jwks.json
维护公钥集合,支持密钥无感轮换,无需服务端改配置、重启服务,是生产环境微服务、SSO
的最优方案。
sequenceDiagram
participant 客户端
participant 授权中心
participant 业务服务
participant JWKS公钥地址
客户端->>授权中心: 1.账号密码登录
授权中心-->>授权中心: 2.私钥生成带kid的JWT令牌
授权中心->>客户端: 3.返回JWT令牌
客户端->>业务服务: 4.请求业务接口+携带JWT
业务服务->>JWKS公钥地址: 5.远程拉取最新公钥集合
JWKS公钥地址->>业务服务: 6.返回JWKS公钥列表
业务服务-->>业务服务: 7.根据JWT的kid匹配对应公钥
业务服务-->>业务服务: 8.公钥验签+校验令牌过期
alt 校验通过
业务服务->>客户端: 9.放行并返回业务数据
else 校验失败
业务服务->>客户端: 10.返回401未授权
end
四、JWKS 公钥网址核心作用
很多开发者疑惑:无 Kid 的 Token
如何验签?公钥网址的核心价值是什么? 这里做精准总结:
无 Kid
场景 :系统约定仅有唯一公私钥对,业务服务本地硬编码公钥,直接固定公钥验签,无需
JWKS 地址,缺点是无法平滑轮换密钥,换密钥需重启所有服务。
有 Kid+JWKS 场景 :公钥网址存储多组公钥,通过 Kid
精准匹配验签,支持新旧密钥共存,实现密钥无感轮换 ,业务零中断。
统一验签标准:适配 Keycloak、Auth0
等第三方授权中心,所有微服务统一从公钥地址拉取密钥,无需单独配置。
防止令牌伪造:签名由私钥生成,公钥公开仅用于验签,无法篡改令牌内容。
五、Node.js 实战:JWKS
远程拉取公钥验签
该代码实现自动从 JWKS 公钥地址拉取公钥、根据 Kid
匹配密钥、自动验签,适配标准 JWT 分布式场景。
5.1 安装依赖
1 npm install jsonwebtoken jwks-rsa
5.2 完整验签代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 const jwt = require ('jsonwebtoken' );const jwksClient = require ('jwks-rsa' );const jwksUri = 'https://xxx.com/.well-known/jwks.json' ;const client = jwksClient ({ jwksUri : jwksUri });function getKey (header, callback ) { client.getSigningKey (header.kid , (err, key ) => { if (err) return callback (err); const signingKey = key.publicKey || key.rsaPublicKey ; callback (null , signingKey); }); }function verifyToken (token ) { return new Promise ((resolve, reject ) => { jwt.verify (token, getKey, { algorithms : ['RS256' ] }, (err, decoded ) => { if (err) return reject (err); resolve (decoded); }); }); }const token = '你的JWT令牌' ;verifyToken (token) .then (user => { console .log ('校验成功,用户信息:' , user); }) .catch (err => { console .log ('校验失败:' , err.message ); });
六、SpringBoot
实战:JWKS 远程验签 + 本地公钥验签
6.1 核心依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-security</artifactId > </dependency > <dependency > <groupId > io.jsonwebtoken</groupId > <artifactId > jjwt-api</artifactId > <version > 0.11.5</version > </dependency > <dependency > <groupId > io.jsonwebtoken</groupId > <artifactId > jjwt-impl</artifactId > <version > 0.11.5</version > <scope > runtime</scope > </dependency > <dependency > <groupId > io.jsonwebtoken</groupId > <artifactId > jjwt-jackson</artifactId > <version > 0.11.5</version > <scope > runtime</scope > </dependency > <dependency > <groupId > com.nimbusds</groupId > <artifactId > nimbus-jose-jwt</artifactId > <version > 9.37.3</version > </dependency >
6.2 配置文件(application.yml)
1 2 3 4 5 6 7 8 9 jwt: jwks-uri: https://xxx.com/.well-known/jwks.json public-key: | -----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQE... -----END PUBLIC KEY----- algorithm: RS256
6.3 模式一:JWKS
远程自动验签(支持 Kid、密钥轮换)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 import com.nimbusds.jose.JWSAlgorithm;import com.nimbusds.jose.jwk.JWKSet;import com.nimbusds.jose.jwk.source.ImmutableJWKSet;import com.nimbusds.jose.jwk.source.JWKSource;import com.nimbusds.jose.proc.JWSKeySelector;import com.nimbusds.jose.proc.JWSVerificationKeySelector;import com.nimbusds.jose.proc.SecurityContext;import com.nimbusds.jwt.JWTClaimsSet;import com.nimbusds.jwt.proc.ConfigurableJWTProcessor;import com.nimbusds.jwt.proc.DefaultJWTProcessor;import org.springframework.beans.factory.annotation.Value;import org.springframework.stereotype.Component;import javax.annotation.PostConstruct;import java.net.URL;@Component public class JwksJwtUtil { @Value("${jwt.jwks-uri}") private String jwksUri; private ConfigurableJWTProcessor<SecurityContext> jwtProcessor; @PostConstruct public void init () throws Exception { JWKSet jwkSet = JWKSet.load(new URL (jwksUri)); JWKSource<SecurityContext> jwkSource = new ImmutableJWKSet <>(jwkSet); JWSKeySelector<SecurityContext> keySelector = new JWSVerificationKeySelector <>(JWSAlgorithm.RS256, jwkSource); jwtProcessor = new DefaultJWTProcessor <>(); jwtProcessor.setJWSKeySelector(keySelector); } public JWTClaimsSet verifyToken (String token) throws Exception { return jwtProcessor.process(token, null ); } }
6.4 模式二:本地公钥验签(无
Kid 单体场景)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 import io.jsonwebtoken.Claims;import io.jsonwebtoken.Jws;import io.jsonwebtoken.Jwts;import io.jsonwebtoken.security.Keys;import org.springframework.beans.factory.annotation.Value;import org.springframework.stereotype.Component;import jakarta.annotation.PostConstruct;import java.security.PublicKey;import java.util.Base64;@Component public class JwtNoKidUtil { @Value("${jwt.public-key}") private String publicKeyStr; private PublicKey publicKey; @PostConstruct public void init () { byte [] keyBytes = Base64.getMimeDecoder().decode( publicKeyStr.replace("-----BEGIN PUBLIC KEY-----" ,"" ) .replace("-----END PUBLIC KEY-----" ,"" ) ); publicKey = Keys.x509(keyBytes); } public Jws<Claims> verify (String token) { return Jwts.parserBuilder() .setSigningKey(publicKey) .build() .parseClaimsJws(token); } }
6.5 全局鉴权拦截器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 import com.nimbusds.jwt.JWTClaimsSet;import org.springframework.stereotype.Component;import org.springframework.web.servlet.HandlerInterceptor;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;@Component public class JwtAuthInterceptor implements HandlerInterceptor { private final JwksJwtUtil jwksJwtUtil; public JwtAuthInterceptor (JwksJwtUtil jwksJwtUtil) { this .jwksJwtUtil = jwksJwtUtil; } @Override public boolean preHandle (HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String authHeader = request.getHeader("Authorization" ); if (authHeader == null || !authHeader.startsWith("Bearer " )) { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); return false ; } String token = authHeader.substring(7 ); try { JWTClaimsSet claims = jwksJwtUtil.verifyToken(token); request.setAttribute("userId" , claims.getSubject()); request.setAttribute("role" , claims.getClaim("role" )); } catch (Exception e) { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); return false ; } return true ; } }
6.6 拦截器注册配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import org.springframework.context.annotation.Configuration;import org.springframework.web.servlet.config.annotation.InterceptorRegistry;import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;@Configuration public class WebConfig implements WebMvcConfigurer { private final JwtAuthInterceptor jwtAuthInterceptor; public WebConfig (JwtAuthInterceptor jwtAuthInterceptor) { this .jwtAuthInterceptor = jwtAuthInterceptor; } @Override public void addInterceptors (InterceptorRegistry registry) { registry.addInterceptor(jwtAuthInterceptor) .addPathPatterns("/**" ) .excludePathPatterns("/login" ); } }
七、核心知识点总结对比
适用场景
单体项目、密钥长期不变
微服务、SSO、第三方授权、需密钥轮换
公钥获取方式
本地配置文件硬编码
远程拉取 jwks.json 公钥集合
密钥轮换
不支持,换密钥需重启所有服务、用户强制下线
支持无感轮换,新旧密钥共存,业务零中断
扩展性
差,仅支持单密钥
极强,支持多密钥并行验签
八、最终核心结论
1. JWT 无 Kid
时,服务端不做密钥选择 ,直接使用本地唯一公钥验签,架构简单但无法平滑迭代;
2. JWT 带 Kid + JWKS 公钥网址是生产环境标准方案,依靠 Kid
精准匹配公钥,实现分布式系统统一鉴权、密钥无感更新;
3. 验签逻辑永远在服务端执行 ,前端仅负责存储和携带
Token,永不参与签名校验;
4. RS256 非对称加密是企业级首选,私钥签名、公钥验签,安全性远高于
HS256 对称加密。