深入理解 JWT:从 Token 结构、签名机制到前后端鉴权实战

摘要:JWT(JSON Web Token,RFC 7519 定义的轻量级令牌标准)是现代前后端分离与微服务架构中主流的鉴权方案。本文从 Token 的三段结构讲起,拆解 Base64URL 与签名防篡改原理,对比 HS256 与 RS256,逐行演示 Node 与 Java 的可运行签验代码,并覆盖 none 攻击、刷新令牌、Spring 网关统一鉴权等实战要点。

导语

做过登录的同学大多遇到过这样的尴尬:传统 Session + Cookie 在单机跑得好好的,一旦上了负载均衡、上了微服务,会话就得在多个节点间同步,要么引入 Redis 集中存储,要么被迫做粘性会话。跨域、跨端(Web / App / 小程序)的场景下,Cookie 更是处处掣肘。

JWT 给出的答案是:把用户状态编码进令牌本身,服务端不再保存会话。令牌自带签名,任何一端拿到后都能独立验签,无需回查中心化存储。但它绝不是"银弹"------用错算法、存错位置、不设置过期,都会让安全形同虚设。下面我们从结构开始,把它彻底讲透。

一、为什么需要 JWT:Session/Cookie 的痛点

传统会话方案依赖服务端状态。用户登录后,服务器在内存或 Redis 里维护一份 sessionId -> 用户数据 的映射,浏览器靠 Cookie 携带 sessionId

这种方式的痛点集中在三点:

  • 水平扩展难:多实例部署时,会话必须共享存储(如 Redis),否则一次请求落到不同节点就会"掉登录"。
  • 跨域/跨端弱Cookie 受同源策略约束,App、小程序、第三方服务很难直接使用。
  • 中心化依赖:每次请求都要查一次会话存储,存储挂了鉴权就挂了。

JWT 把"服务端保存状态"变成"令牌自带状态 + 签名保真"。服务端无状态验签即可确认身份,天然适配微服务与多端场景。RFC 7519 把这种令牌定义为一种紧凑的、URL 安全的声明(claim)传输格式。

声明(claim)是 JWT 里的一个键值对,比如 sub(主题)、exp(过期时间),可以理解为令牌携带的"身份信息字段"。

二、JWT 的三段结构:Header / Payload / Signature

一个 JWT 看起来就是一串用小数点分隔的文本:xxxxx.yyyyy.zzzzz。它严格由三段组成,分别对应 Header(头)、Payload(载荷)、Signature(签名)

  • Header :描述元数据,通常含 alg(签名算法,如 HS256 / RS256)和 typ(令牌类型,固定为 JWT)。
  • Payload:存放实际业务声明,如用户 ID、角色、过期时间等标准 claim。
  • Signature :对前两段的签名,用来防篡改------任何对 Header 或 Payload 的修改都会让签名校验失败。

关键点:Payload 只是 Base64URL 编码,不是加密。任何人都能解码看到里面的内容(下文会演示),所以它绝不能放密码、身份证号等敏感明文。JWT 保证的是"内容没被改过",而不是"内容别人看不到"。

text 复制代码
Header    : {"alg":"HS256","typ":"JWT"}        -- Base64URL 编码
Payload   : {"sub":"user-1001","exp":...}       -- Base64URL 编码
Signature : HMAC-SHA256(base64url(header)+"."+base64url(payload), 密钥)
最终令牌   : base64url(header) + "." + base64url(payload) + "." + base64url(signature)

三、Base64URL 编解码演示(Node 手动拆解)

JWT 用的不是普通 Base64,而是 Base64URL :它把标准 Base64 里的 + 换成 -/ 换成 _,并去掉结尾的 =,目的是让令牌能安全出现在 URL、Header 中而不被转义。

下面用 Node 的 Buffer 手动解码一段真实 JWT 的 Payload,看清"明文可读"这件事:

javascript 复制代码
// 取一段真实 JWT 的 Payload 部分(已省略首尾的 .)
const part = 'eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ';

// Base64URL -> 标准 Base64:把 - 换 +,_ 换 /,再补回 = 使长度为 4 的倍数
const b64 = part.replace(/-/g, '+').replace(/_/g, '/') + '==='.slice((part.length + 3) % 4);
const json = Buffer.from(b64, 'base64').toString('utf8');
console.log(JSON.parse(json));
// 输出: { sub: '1234567890', name: 'John Doe', iat: 1516239022 }

运行后你会发现,Payload 里的内容一览无余。这正是为什么上文强调"Payload 不加密、只防篡改"。如果业务要求内容保密,应当改用 JWE(加密令牌)或对敏感字段另行加密,而不是指望 JWT 的编码来隐藏信息。

四、签名机制:HS256 与 RS256,以及验签流程

签名是 JWT 安全的命门。主流算法有两类:

  • HS256(HMAC-SHA256) :对称算法,签发和验签用同一把密钥。实现简单,适合单服务内部。
  • RS256(RSASSA-PKCS1-v1_5 with SHA-256) :非对称算法,用私钥签名、公钥验签。验签方只需持有公钥,私钥永不外泄,天然适合多服务、第三方接入和微服务网关统一鉴权。

JWKS(JSON Web Key Set)是一种公钥分发机制:鉴权服务把 RS256 的公钥以标准 JSON 暴露出来,各资源服务定时拉取,实现密钥轮换而不必硬编码公钥。

验签的本质流程如下,资源服务器每一步都不能省:

text 复制代码
客户端                    资源服务器
  |-- Authorization -------->|
  |   Bearer <token>        | 1. 按 "." 拆成 Header / Payload / Signature
  |                         | 2. 用密钥/公钥 重算 Signature
  |                         | 3. 比对:不一致 => 401(被篡改)
  |                         | 4. 校验 exp/iat/nbf/aud/iss 等声明
  |                         | 5. 全部通过 => 返回受保护资源

下面用 Node 的 jsonwebtoken 给出可运行 的签验代码,重点是验签时必须显式白名单算法,杜绝 none 与算法混淆:

javascript 复制代码
const jwt = require('jsonwebtoken');

// 至少 32 字节的随机密钥(HS256 用);生产环境应从配置中心读取
const secret = 'a-very-long-random-secret-at-least-32-bytes!!';
const payload = { sub: 'user-1001', role: 'admin', aud: 'web-app', iss: 'auth-svc' };

// 签发:显式指定算法 HS256,设置 2 小时过期
const token = jwt.sign(payload, secret, {
  algorithm: 'HS256',
  expiresIn: '2h',
});

// 验签:必须显式白名单算法,杜绝 none / 算法混淆;同时校验 exp/aud/iss
try {
  const decoded = jwt.verify(token, secret, {
    algorithms: ['HS256'],   // 禁止 none、禁止把 RS256 误当 HS256
    audience: 'web-app',      // 校验受众,防令牌跨服务冒用
    issuer: 'auth-svc',       // 校验签发方
  });
  console.log('验签通过:', decoded);
} catch (e) {
  console.error('验签失败:', e.message);
}

// RS256 变体:私钥签发、公钥验签(密钥换成文件读取的 PEM)
// const token = jwt.sign(payload, privateKey, { algorithm: 'RS256', expiresIn: '2h' });
// const decoded = jwt.verify(token, publicKey, { algorithms: ['RS256'], audience: 'web-app' });

algorithmsaudienceissuer 三个选项缺一不可:algorithms 锁死算法,audience/issuer 防止 A 服务签发的令牌被 B 服务越权接受。

五、常见攻击与防护:none 算法、算法混淆、弱密钥

JWT 历史上出过不少安全事件,核心几乎都绕不开"算法被操控"。下面这张表把常见攻击与对应防护列清楚:

攻击方式 原理 防护手段
none 算法 alg 改成 none,声称无需签名即可通过验签 验签显式白名单 algorithms,直接拒绝 none
算法混淆 RS256→HS256 攻击者用公开的公钥当 HMAC 密钥重签,骗服务端按 HS256 验 验签固定 algorithms: ['RS256'],密钥与算法绑定
弱密钥 HS256 密钥太短或可猜(如 secret123)被暴力破解 使用 ≥32 字节的随机密钥,不从代码硬编码
令牌泄露 XSS 从 localStorage 偷走令牌 httpOnly Cookie + HTTPS,缩短令牌时效

最容易被忽视的是 HTTPS 的强制性 。JWT 在传输中是明文令牌,一旦被中间人截获就能冒用,所以鉴权接口与令牌传输必须全程 TLS。此外,永远不要在前端用 alg: none 的库配置,也不要根据令牌里的 alg 字段动态选择验签密钥------这正是算法混淆漏洞的根源。

六、令牌过期与刷新:短时效 Access + Refresh

即便令牌泄露,也要把损失控制在"一小段时间"内。业界标准做法是双令牌

  • Access Token:短时效(如 15 分钟~2 小时),用于日常接口鉴权。
  • Refresh Token:较长时效(如 7~30 天),仅用于在 Access Token 过期后换取新令牌,通常只走一次性换发接口。

主动吊销(用户登出、被盗)则需要服务端配合。常见方案是用 Redis 维护一个吊销名单,以 jti(JWT ID,令牌唯一标识)为键:

bash 复制代码
# 吊销:把 jti 写入黑名单,过期时间与令牌剩余寿命一致
SETEX jwt:blacklist:<jti> <剩余秒数> 1

# 校验时先查黑名单,存在即拒绝
EXISTS jwt:blacklist:<jti>   # 返回 1 表示已吊销

另外两点工程细节:刷新令牌本身应当支持旋转 (每次换发都下发新的 Refresh Token,旧的直接失效),以及处理时钟漂移 ------多机部署时各节点时间可能偏差几秒,验签 exp/nbf 时建议预留几秒容差,避免临界点频繁 401。

七、前后端实战:Bearer、存储与 Spring 统一鉴权

前端拿到令牌后,标准做法是在请求头里带上它,格式为 Authorization: Bearer <token>。真正棘手的是存哪里

  • localStorage:读取方便,但任何 XSS 脚本都能读取,风险高。
  • httpOnly Cookie :JS 读不到,能挡 XSS 偷令牌,但要额外防 CSRF (配合 SameSite 与 CSRF Token 缓解)。

折中方案是:Access Token 放内存(或短期 localStorage),Refresh Token 放 httpOnly Cookie。无论哪种,全程 HTTPS 是前提。

后端侧,以 Spring 生态为例,推荐用 OncePerRequestFilter 做统一鉴权,让业务 Controller 无感知:

java 复制代码
@Component
public class JwtAuthFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain)
            throws java.io.IOException, jakarta.servlet.ServletException {
        String auth = req.getHeader("Authorization");
        if (auth != null && auth.startsWith("Bearer ")) {
            String token = auth.substring(7);
            try {
                Claims claims = JwtDemo.verify(token); // 复用上文 jjwt 的验签方法
                var authToken = new UsernamePasswordAuthenticationToken(
                        claims.getSubject(), null,
                        java.util.List.of(new SimpleGrantedAuthority("ROLE_" + claims.get("role"))));
                SecurityContextHolder.getContext().setAuthentication(authToken);
            } catch (JwtException e) {
                res.sendError(HttpServletResponse.SC_UNAUTHORIZED, "invalid token");
                return;
            }
        }
        chain.doFilter(req, res);
    }
}

微服务网关 (如 Spring Cloud Gateway)层统一做这件事收益最大:网关校验 JWT 后,把解析出的 sub、角色等声明以请求头透传给下游服务,下游服务只需信任网关注入的身份,不必每个服务都重复实现鉴权。配合 JJWT 的签验代码如下(依赖 io.jsonwebtoken:jjwt-api:0.11.5 等三个 artifact):

java 复制代码
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import java.security.Key;
import java.util.Date;

public class JwtDemo {
    // HS256 用对称密钥(≥32 字节);RS256 则换成私钥签发、公钥验签
    static final Key KEY = Keys.hmacShaKeyFor(
            "a-very-long-random-secret-at-least-32-bytes!!".getBytes());

    public static String issue() {
        long now = System.currentTimeMillis();
        Date exp = new Date(now + 2 * 60 * 60 * 1000L); // 2 小时过期
        return Jwts.builder()
                .setSubject("user-1001")
                .setIssuer("auth-svc")
                .setAudience("web-app")
                .setIssuedAt(new Date(now))
                .setExpiration(exp)
                .setId("jti-abc-123")                 // jti,用于黑名单吊销
                .signWith(KEY, SignatureAlgorithm.HS256)
                .compact();
    }

    public static Claims verify(String token) {
        return Jwts.parserBuilder()
                .requireAudience("web-app")
                .requireIssuer("auth-svc")
                .setSigningKey(KEY)                   // RS256 时换成公钥
                .build()
                .parseClaimsJws(token)
                .getBody();
    }
}
xml 复制代码
<!-- Maven 依赖(jjwt 0.11.5,三个 artifact 缺一不可) -->
<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>

八、总结与最佳实践清单

JWT 把"状态"从服务端搬进了令牌,让无状态鉴权在多端、微服务场景下变得简单。但便利的另一面是:一旦算法、存储、过期任一环出错,风险立刻放大。把下面这份清单贴在工位上,落地时逐条对照:

  1. 始终 HTTPS:令牌明文传输,TLS 是底线。
  2. 验签白名单算法 :永远显式传 algorithms,拒绝 none,HS256 / RS256 各自锁死。
  3. 校验声明exp / aud / iss 必须验,跨服务别让令牌乱用。
  4. 短时效 Access + 长时效 Refresh:Access 泄露也只亏一小段时间。
  5. 可吊销 :用 jti + Redis 黑名单支持登出/被盗紧急失效。
  6. 敏感信息别进 Payload:它只是编码不是加密,密码、证件号另做处理。
  7. 前端存储谨慎 :Refresh Token 放 httpOnly Cookie,降低 XSS 偷令牌风险。
  8. 网关统一鉴权:在网关层收敛 JWT 校验,下游服务信任透传身份。

如果你正在搭建 Spring Security 鉴权体系,建议把网关统一验签与本文的 Refresh 机制一起设计,避免每个服务重复造轮子。读懂结构、用对算法、管好生命周期,JWT 就能稳稳撑起你的前后端鉴权。


参考资料

© 2024 | 转载请注明出处

相关推荐
OCR_133716212751 小时前
ICAO9303 标准解读:护照 OCR 数据查验如何识破高仿变造证件
安全·智能硬件
一只小李郁vickie1 小时前
Redis 集群部署手册(3主3从)
运维·redis
QQ_21696290961 小时前
基于SpringBoot+Vue的小生活平台的设计与实现
java·数据库·vue.js·spring boot·spring·微信小程序·生活
yxlalm1 小时前
SpringAI+RAG-检索文档变知识:从上传到精准检索的完整链路
spring·知识库·rag
Wang's Blog10 小时前
Java框架快速入门: Spring Security+OAuth2之数据库和实体类的RBAC改造
java·数据库·spring
2601_9623649712 小时前
面向设备厂商:Windows Hello 认证摄像头核心要点
windows·安全·电脑
猫吻鱼12 小时前
【AI 01】【Spring AI 基础使用】
java·spring·spring ai
Gl�ria13 小时前
Redis 高可用架构对比
数据库·redis
云边云科技_云网融合13 小时前
工业互联网组网方案全景解析:从低延迟网络到安全隔离的四大核心支柱
网络·安全·php