一文搞懂:JWT(JSON Web Token)与Token认证——从结构剖析到签名算法,再到刷新与注销全攻略
📌 写在前面
在传统的Web开发中,Session认证一直是主流:用户登录成功,服务器在内存中存一份Session记录,客户端拿到一个Session ID,后续请求带着这个ID来“认亲”。这套模式在单机时代运转良好,但到了分布式、微服务、前后端分离的年代,问题就来了——Session需要共享存储,跨域请求麻烦,扩展性受限。
这时候,JWT(JSON Web Token)走进了我们的视野。它是一种无状态的认证方案,服务器不需要存储任何会话信息,所有的用户凭证和权限都“打包”在Token里,客户端自己带着。这个特性天然适配分布式系统和跨域场景,一度被视为Session的终结者。但随着深入使用,JWT的“坑”也渐渐浮出水面:Token一旦签发就无法主动撤销,注销成了难题;用localStorage存Token容易遭到XSS攻击;签名算法选错了可能直接让整个认证体系形同虚设……很多人照着网上教程“配好就跑”,上线后埋下了巨大的安全隐患。
这篇笔记,我们从JWT的诞生背景出发,逐步拆解它的结构、签名算法、安全存储方案、 Refresh Token 刷新机制以及注销的应对策略,最后对比它与Session的优缺点。希望通过这篇文章,你能既会用JWT,也知道“为什么这么用”。
1️⃣ JWT 核心概念与结构解析
JWT(JSON Web Token)是由 RFC 7519 标准定义的一种紧凑且自包含的令牌格式,用于在网络双方之间安全传输信息。
紧凑(Compact):JWT 是一个很短的字符串,可以轻松放在 URL 参数、HTTP Authorization 请求头或请求体中传输。
自包含(Self-contained):JWT 的 Payload 部分包含了用户的所有必要信息(如用户 ID、角色、权限等),服务端收到 Token 后可以直接解析使用,无需查询数据库或缓存。
JWT 的三段式结构
一个标准的 JWT 长这样:
<Header>.<Payload>.<Signature>三个部分分别用.隔开,各自经过Base64URL 编码。
第一段:Header(头部)
Header 是一个 JSON 对象,包含两个主要字段:alg(签名算法)和typ(令牌类型,通常固定为JWT)。
{ "alg": "HS256", "typ": "JWT" }Header 经过 Base64URL 编码后,变成了 JWT 的第一部分。
第二段:Payload(载荷)
Payload 存储的是声明(Claims),也就是你想要附加的信息。JWT 标准预定义了一组“注册声明”(Registered Claims),在工程落地时有极高的规范和审查价值:
| 声明 | 全称 | 含义 | 强制性 |
| :--- | :--- | :--- | |
|iss| Issuer | JWT 的签发者(例如认证服务的域名) | 推荐校验 |
|sub| Subject | JWT 的主题(一般是用户的唯一标识,如用户 ID) | 推荐 |
|aud| Audience | JWT 的接收方(指定哪些服务可以使用该 Token) | 高 |
|exp| Expiration Time | 过期时间(Unix 时间戳,必须要校验,否则 Token 永不过期) |必须|
|iat| Issued At | 签发时间 | 推荐 |
|nbf| Not Before | 生效时间(该时间之前 Token 不可用) | 按需 |
|jti| JWT ID | 令牌的唯一标识,用于防止重放攻击和黑名单吊销 | 按需 |
{ "sub": "1234567890", "name": "John Doe", "admin": true, "iat": 1516239022 }Payload 同样经过 Base64URL 编码,成为 JWT 的第二部分。
第三段:Signature(签名)
签名的作用是防篡改。它通过对 Header 和 Payload 进行签名运算生成,任何人对 Header 或 Payload 的修改都会让签名失效。
signature = HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret )服务端收到 JWT 后,会用同样的密钥和算法重新计算签名,与 JWT 附带的签名比对,一致则证明 Token 未被篡改。
⚠️关键提醒:JWT 的 Payload 只是 Base64 编码,不是加密。任何人都可以解码读取其中的内容,严禁在 Payload 中存放密码、身份证号、信用卡号等敏感信息。
2️⃣ JWT 签名算法详解与选型指南
JWT 的安全性很大程度上取决于签名算法的选择。目前主流的算法分为两大类:对称加密(HMAC)和非对称加密(RSA/ECDSA/EdDSA)。
2.1 HS256(HMAC with SHA-256)——对称加密
原理:使用同一个密钥(Secret)进行签名和验证。签发者和验证者必须共享这个密钥。
优点:
计算效率高:签名和验证都很快,验证环节甚至能达到 HS256 比 RS256 快一个数量级的程度。
实现简单:适合单服务或内部系统,所有参与方在同一个信任边界内。
缺点:
密钥分发困难:系统中所有需要验证 Token 的服务都必须持有同一个密钥,一旦密钥泄露,任何人都可以签发伪造的 Token。
不适合分布式/多服务架构:在多服务场景下,共享密钥会带来安全隐患和管理成本。
适用场景:单一服务或所有验证方都在同一信任边界内、性能要求极高的场景。
2.2 RS256(RSA with SHA-256)——非对称加密
原理:使用一对公私钥——私钥用于签名,公钥用于验证。签发者(认证服务)持有私钥,验证者(各业务服务)只持有公钥。
优点:
安全边界清晰:即使公钥泄露,攻击者也无法签发伪造的 Token(因为私钥没有泄露)。
天然适合微服务架构:认证服务统一签发 Token,各个业务服务只需配置公钥即可验证,无需共享同一个密钥。
缺点:
性能略低于 HS256:签名过程相对较慢。
实现略复杂:需要管理公私钥对。
适用场景:微服务架构、多服务协作、需要安全边界隔离的场景。
2.3 更现代的推荐:EdDSA(Ed25519)
2025 年的安全实践逐渐将 EdDSA 列为 JWT 签名算法的首选。相比于 RS256,Ed25519 在多服务分离的前提下,还具备签名长度更短、签名验证性能更高的优势。
2.4 签名算法选型对比表![]()
2026 年推荐方案:除非在单一服务内对性能要求极致,否则不推荐 HS256,特别是微服务架构应首选 Ed25519 或 RS256。如果使用非对称算法,务必在验证时限定只允许预期的算法,防止攻击者在令牌 Header 中指定alg=none绕过签名校验。
3️⃣ JWT 与 Session 对比
JWT 与 Session 是两种主流的认证方式,本质上不是“谁更好”的问题,而是“在不同的分布式和工程管道下谁更合适”。
一句话总结:Session 适合需要实时控制状态的场景(如金融交易、后台管理系统);JWT 适合分布式、高扩展性的场景(如微服务、移动端 API)。
4️⃣ JWT 安全存储与风险防控
JWT 的安全性不仅仅在于签名算法,更在于Token 在客户端的存储方式。
4.1 错误示范:localStorage 存储
很多前端项目为了方便,直接将 JWT 存进localStorage。
// ❌ 危险的做法 localStorage.setItem('token', jwtToken);这种方式有一个致命的弱点——localStorage里的数据可以被同一域名下的任何 JavaScript 代码读取。如果网站存在一个 XSS(跨站脚本攻击)漏洞,攻击者可以轻而易举地把所有用户的 Token 偷走。
4.2 推荐方案:HttpOnly Cookie
将 JWT 存储在带有HttpOnly属性的 Cookie 中,可以最大程度地隔离 XSS 风险。
// ✅ 安全做法:服务器设置 Cookie res.cookie('token', jwtToken, { httpOnly: true, // JavaScript 无法访问 secure: true, // 仅 HTTPS 传输 sameSite: 'strict', // 防止 CSRF maxAge: 24 * 60 * 60 * 1000 });HttpOnly确保 Cookie 不能被客户端的 JavaScript 读取,即使网站存在 XSS 漏洞,攻击者也拿不到 Token。存储方案执行后,理论上需要在验证时统一从 Cookie 中读取 JWT。
2026 年更为前沿的选择:在 SPA 单页架构中,可利用Web Worker 的隔离沙箱,将用户令牌与主渲染线程彻底分离,大幅降低扩展内存注入的暴露面;但这种模式下,Token 的自动携带和透明刷新复杂度更高,也引入了双向通信时序负担。
5️⃣ Refresh Token 机制与无感刷新实战
JWT 安全实践的第一准则是:Access Token 的生命周期极短(通常设为 15-30 分钟),以减少 Token 泄露后的危害。
但 Token 频繁过期会让用户体验极差——每 15 分钟就要重新登录一次。Refresh Token(刷新令牌)+令牌自动续期流程解决了这个问题。
5.1 双 Token 机制
Access Token:短期有效(15分钟 - 2小时),用于访问资源,无状态验证。
Refresh Token:长期有效(7天 - 30天),存储在服务端或 HttpOnly Cookie 中,仅在 Access Token 过期时换取新的 Access Token。
5.2 自动刷新流程图
5.3 前端无感刷新最佳实践
在前端应用中(以 Axios 为例),可以在响应拦截器中自动处理 Token 过期与刷新:
axios.interceptors.response.use( response => response, async error => { const originalRequest = error.config; if (error.response.status === 401 && !originalRequest._retry) { originalRequest._retry = true; try { const refreshToken = getRefreshToken(); // 从 Cookie 或存储中获取 const newToken = await api.refreshToken(refreshToken); setAccessToken(newToken); originalRequest.headers['Authorization'] = `Bearer ${newToken}`; return axios(originalRequest); } catch (refreshError) { redirectToLogin(); } } return Promise.reject(error); } );整个过程对用户完全透明,实现了“无感刷新”。
5.4 Refresh Token 安全设计要点
短有效期结合旋转策略:Access Token 设计为 15 分钟,Refresh Token 设置为 7-30 天,但须限制 Refresh Token 不可用于业务接口访问。
一次性使用 + 设备绑定:每次使用 Refresh Token 换取新的 Access Token 后,应将旧的 Refresh Token 废弃,并生成新的 Refresh Token(Token 轮换),同时限制单个 Refresh Token 只能绑定登录设备,防止在多端窃取场景下通过 Refresh Token 横向扩散。
存储安全:Refresh Token 原则上不应放在 localStorage 中,而是放入 HttpOnly Cookie 甚至服务端的“令牌仓库”中统一管理。
6️⃣ 注销登出:JWT 无法主动失效的应对方案
JWT 最大的痛点就是:标准 JWT 一旦签发,在有效期内无法从服务端主动使其失效。
6.1 方案一:Redis 黑名单(最推荐,适合中小型系统)
原理:用户登出时,将该 Token 的唯一标识(jti)存入 Redis 黑名单,过期时间与 Token 剩余有效期一致。每次请求验证 Token 时,先检查黑名单。
// 1. 生成 Token 时,添加唯一标识 jti String jwt = Jwts.builder() .setId(UUID.randomUUID().toString()) // 唯一 ID .setSubject("user123") .setExpiration(new Date(System.currentTimeMillis() + 3600000)) .signWith(SignatureAlgorithm.HS256, secretKey) .compact(); // 2. 登出时,将 jti 加入 Redis 黑名单 public void logout(String jwt) { Claims claims = Jwts.parser() .setSigningKey(secretKey) .parseClaimsJws(jwt) .getBody(); // jti 作为 key,过期时间与 JWT 剩余有效期一致 redisTemplate.opsForValue().set( "blacklist:" + claims.getId(), "", getRemainingTTL(jwt), TimeUnit.MILLISECONDS ); } // 3. 请求拦截器中校验黑名单 public boolean isTokenBlacklisted(String jwt) { Claims claims = parseJWT(jwt); return redisTemplate.hasKey("blacklist:" + claims.getId()); }核心要点:Redis 中黑名单项的过期时间(TTL)必须与 JWT 本身的剩余有效期精确对齐,否则会出现“Token 已过期但黑名单先清空”或“未过期却被误拦”的断裂。
6.2 方案二:用户版本号机制
在用户表中维护一个token_version字段,用户每次登录或注销时递增。JWT 的 Payload 中包含这个版本号,每次请求时与数据库中的版本号比对。
优点:可以一性令某个用户的所有Token 同时失效(如修改密码场景);缺点:每次请求都要查数据库,给后续链带上了新的性能开销。
6.3 方案对比与选型建议
推荐:对于大多数场景,优先选择Redis 黑名单 + 短生命周期 Access Token组合;如果非常看重“实时吊销能力”,可以配合 Refresh Token 旋转。
7️⃣ Spring Boot + JWT 实战流程
在 Spring Boot 中集成 JWT,通常需要以下步骤(基于jjwt依赖):
引入依赖:在
pom.xml中添加jjwt。编写 JWT 工具类:实现 Token 的生成、解析、验证。
配置 Spring Security:设置哪些接口需要认证、哪些放行。
实现认证过滤器:从请求中提取 JWT 并验证。
实现登录接口:验证用户名密码后生成 JWT 并返回。
核心过滤器(OncePerRequestFilter)的工作流程如下:
关于实现细节的补充:Spring 生态中可借助spring-boot-starter-security组件配合JwtAuthenticationToken,用 JWT 来承载用户凭证与权限集合。实践中应当避免在 Filter 链中进行过度的数据库查询(例如在每次请求中读取用户全部角色),建议将必要的权限声明精简后预置进 Token Payload。
8️⃣ 总结与最佳实践
2026 年的安全趋势:业界正在超越传统 JWT,比如PASETO(平台无关安全令牌)强制绑定了算法与协议版本,从根本上杜绝了算法混淆。 不过 JWT 凭借 OAuth 2.0 / OIDC 生态和云服务商的广泛支持,在大部分无状态加解密场景中仍被作为兜底方案广泛采用。
在实际项目中,假如你的团队使用 JWT 做认证,Access Token 设置为 1 小时过期,Refresh Token 设置为 14 天,Refresh Token 存储在 HttpOnly Cookie 中。运营团队在 12 小时后发现某个用户的账号在另一个设备上被恶意登录,要求立即踢掉该用户的所有端登录状态。如果利用 Refresh Token 轮换设计和 Redis 黑名单两个存储组件,你该怎么在不动用额外时钟、不影响其他用户的前提下快速落地?(欢迎在评论区分享你的设计方案和踩坑经验)
