JWT 认证完整原理、JWKS 机制与前后端实战代码总结

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');

// 远程JWKS公钥地址
const jwksUri = 'https://xxx.com/.well-known/jwks.json';

// 初始化JWKS客户端,自动缓存公钥、支持密钥轮换
const client = jwksClient({
jwksUri: jwksUri
});

// 根据Token头部kid获取对应公钥
function getKey(header, callback) {
client.getSigningKey(header.kid, (err, key) => {
if (err) return callback(err);
const signingKey = key.publicKey || key.rsaPublicKey;
callback(null, signingKey);
});
}

// JWT异步验签方法
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
<!-- Web、Security基础依赖 -->
<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>

<!-- JJWT核心依赖 -->
<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>

<!-- JWKS远程密钥解析依赖 -->
<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公钥地址(分布式场景)
jwks-uri: https://xxx.com/.well-known/jwks.json
# 本地公钥(单体无Kid场景)
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);
// 初始化验签处理器,自动根据kid匹配公钥
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);
}

// 固定公钥验签,无kid匹配逻辑
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");
}
}

七、核心知识点总结对比

特性 无 Kid + 本地公钥 有 Kid + JWKS 公钥网址
适用场景 单体项目、密钥长期不变 微服务、SSO、第三方授权、需密钥轮换
公钥获取方式 本地配置文件硬编码 远程拉取 jwks.json 公钥集合
密钥轮换 不支持,换密钥需重启所有服务、用户强制下线 支持无感轮换,新旧密钥共存,业务零中断
扩展性 差,仅支持单密钥 极强,支持多密钥并行验签

八、最终核心结论

1. JWT 无 Kid 时,服务端不做密钥选择,直接使用本地唯一公钥验签,架构简单但无法平滑迭代;

2. JWT 带 Kid + JWKS 公钥网址是生产环境标准方案,依靠 Kid 精准匹配公钥,实现分布式系统统一鉴权、密钥无感更新;

3. 验签逻辑永远在服务端执行,前端仅负责存储和携带 Token,永不参与签名校验;

4. RS256 非对称加密是企业级首选,私钥签名、公钥验签,安全性远高于 HS256 对称加密。


JWT 认证完整原理、JWKS 机制与前后端实战代码总结
https://jycpp.github.io/2025/25-10-05-JWT认证完整原理JWKS与前后端实战代码总结.html
作者
Jet Yan
发布于
2025年10月5日
许可协议