⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 http://www.iocoder.cn/Fight/weishenmehenduorenbutuijianniyongJWT/ 「芋道源码」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言都将得到认真回复。甚至不知道如何读源码也可以请教噢。
  4. 新的源码解析文章实时收到通知。每周更新一篇左右。
  5. 认真的源码交流微信群。

先把结论摆桌面:JWT 不是不好,是用错了地方

网上写 JWT 的文章,要么吹爆”无状态、性能好”,要么一棒打死”千万别用”。这俩都偏激。

真实的结论是:JWT 是一项设计精良的技术,但它的最佳战场不是用户登录会话——它本来是为”两端之间传递一次性授权声明”设计的。

不过,配合一些工程手段(比如 Redis 短黑名单),JWT 也能在登录会话场景里跑得很稳。本文要做的,就是把这条工程路径讲清楚——让你不再纠结用不用 JWT,而是知道怎么用得对。

JWT 是什么:30 秒看懂登录流程

JWT 全称 JSON Web Token。本质就是一段 JSON 数据,用密钥签名后让接收方能验证它来自你认识的人。官网:https://jwt.io/

一个 JWT 长这样(三段式:header.payload.signature):

典型登录流程:

  1. 你在网站登录成功,服务器签发一个 JWT 给你
  2. JWT 里塞着你的身份信息:用户名、角色、权限……
  3. 之后你每次请求都把这个 JWT 带上
  4. 服务器只验签名就能信任 JWT 里的信息——不需要查库
  5. 验证通过,请求放行

这个”不查库”是 JWT 最大的卖点——也是后面 4 个约束的根源。

JWT 做会话的 4 个真实约束

会话管理要做的事情:记录用户身份、权限校验、注销/封禁。下面看 JWT 在每一项上的真实表现。

约束 1:体积膨胀,每次请求都带上整个 token

存一个用户 ID xiaou,cookie 只需要 5 字节。塞进 JWT 里就膨胀大约 51 倍:

每次请求都带一遍——会话场景下用户请求频次高,这部分带宽不可忽略。

修法:

  • payload 只塞必要字段——uid + role 够了,别把整个用户对象都塞进去(根本上是减小体积)
  • 配 refresh token + 短 access token——access token 设 15min,refresh token 走更安全通道(HttpOnly Cookie / 单独刷新接口),降低单次暴露面
  • 极敏感场景退回服务端会话——网关用 JWT 鉴权后换发短期不透传的服务端票据,敏感操作走服务端票据
  • ⚠️ 别指望 HTTP 层 gzip——gzip 压缩的是 response body,Authorization Header 和 Cookie 不在压缩范围。HTTP/2 的 HPACK 会做 header 压缩但效果有限,节流的根本还是减小 token 自身大小

约束 2:签名只是 hygiene,不是亮点

“JWT 加密签名让接收方能验证 token 真伪”——但几乎每一个现代 web 框架在使用普通会话 cookie 时都自动加密签名。Spring Session、Express、Django……一个比一个完善。

实际上,绝大多数项目的 JWT 又被放在会话 cookie 里——这意味着同一份数据被签了两次。这不算缺点,只是”它没你想的那么独特”。

约束 3:撤销难,注销不算真正注销(最关键)

这是 JWT 在会话场景下最需要解决的问题。

JWT 是自包含的——服务器没办法主动撤销一个还没到期的 token。后果是:

  • 注销不算真注销:用户点了退出,但泄露出去的 JWT 还能用——直到 token 过期
  • 权限变更不立即生效:管理员降级为普通用户,他的 JWT 里写的还是 admin,继续保留管理员权限直到 token 过期
场景 Session Cookie 原生 JWT
注销立即失效 ✅ 删 session 即可 ❌ 必须等到期
权限变更生效 ✅ 下次请求查库即更新 ❌ token 内容已固化
强制下线 ✅ session 后端控 ❌ 没法强制

修法:下一节的 Redis 短黑名单 专门解这个问题——用 1% 的 Session 内存换回 90% 的撤销能力。

约束 4:默认明文,要靠 HTTPS 兜底

JWT 默认只签名不加密——payload 是 base64 编码的,谁拿到都能解开看里面的内容。

这不是”加密”,是”编码”。如果传输链路被中间人嗅探(HTTPS 配置疏漏、内网代理被攻破等),JWT 等于明文凭证。

修法:

  • 必须走 HTTPS——这是任何 web 应用的底线,不仅是 JWT 的事
  • payload 不放敏感字段——身份证、手机号、地址这种绝对不要塞进 JWT
  • 真要加密——用 JWE(JSON Web Encryption),但工程成本会上升

破解撤销难题:JWT + Redis 短黑名单(最省内存)

撤销难是 JWT 在会话场景的核心痛点——主流解法是”维护一份失效 token 黑名单”。最省内存的实现:

方案:Redis 黑名单 + 短 TTL(按 token 剩余有效期设)

核心思想:黑名单不存所有失效的 token——只存”还没到期但已失效“的。一旦 token 自然过期,自动从黑名单消失。

@Service
public class JwtBlacklistService {

@Autowired
private StringRedisTemplate redisTemplate;

/**
* 用户主动登出 / 强制下线时调用
* @param token JWT token
* @param expiresAt token 自然过期时间戳(秒)
*/
public void revoke(String token, long expiresAt) {
long ttl = expiresAt - System.currentTimeMillis() / 1000;
if (ttl <= 0) return; // 已经过期了,没必要拉黑

// 用 token 的 jti(JWT ID)做 key,避免存全 token 占内存
String jti = JwtUtil.parseJti(token);
redisTemplate.opsForValue().set(
"jwt:blacklist:" + jti,
"1",
ttl, TimeUnit.SECONDS // ⭐ TTL 设为 token 剩余有效期
);
}

public boolean isRevoked(String token) {
String jti = JwtUtil.parseJti(token);
return Boolean.TRUE.equals(
redisTemplate.hasKey("jwt:blacklist:" + jti));
}
}

每次请求验签后多查一次 Redis:

public boolean validateToken(String token) {
if (!JwtUtil.verifySignature(token)) return false;
if (blacklistService.isRevoked(token)) return false; // ⭐ 新增黑名单检查
return true;
}

为什么这套方案省内存

方案 Redis 内存占用 说明
完整 Session 存 Redis 100% 每个用户存完整身份信息(500B+)
JWT + 黑名单(本方案) ~5% 只存”被拉黑的 jti”——绝大多数 token 不在名单里
不维护黑名单(裸 JWT) 0 但失去撤销能力

省内存关键三点:

  • 只存 jti(JWT ID)——一个短字符串,不是整个 token
  • TTL = token 剩余有效期——token 自然过期后黑名单也跟着清,永远不膨胀
  • 黑名单只占小部分——99% 的 token 没被撤销,根本不存 Redis

进阶:批量强制下线(”全量令牌作废”)

特殊场景:密码泄漏需要让某用户所有现有 token 都失效。这时按 jti 拉黑不够——你不知道用户签发过多少 token。

做法:黑名单里存”用户 ID 的 token 失效起点时间”,验签时比对 token 签发时间:

// 把"该用户在 X 时间之前签发的所有 token 视为已撤销"
redisTemplate.opsForValue().set(
"jwt:user-revoked:" + userId,
String.valueOf(System.currentTimeMillis() / 1000),
7L * 24 * 3600, TimeUnit.SECONDS // 7 天 TTL,超过这时间的旧 token 早就过期了
);

// 验签时检查
public boolean validateToken(String token) {
if (!JwtUtil.verifySignature(token)) return false;
long iat = JwtUtil.parseIat(token); // token 签发时间
String userId = JwtUtil.parseSubject(token);
String revokeBefore = redisTemplate.opsForValue().get("jwt:user-revoked:" + userId);
if (revokeBefore != null && iat < Long.parseLong(revokeBefore)) {
return false; // 早于失效起点,撤销
}
return true;
}

选型决策:什么场景用 JWT,什么场景用 Session

场景 推荐 原因
传统单体 Web 项目 Session Cookie 框架内置、撤销简单、零工程成本
跨域/跨服务的 SSO JWT 各服务无状态校验,不用共享 session 存储
移动端 / SPA JWT + Redis 黑名单 移动端 cookie 不便,JWT 更灵活;黑名单解决撤销
微服务体系 JWT + 网关统一验签 网关层解析、下游服务无状态
长会话(30 天+) JWT + Refresh Token access token 短(15min),refresh token 长(30 天)+ Redis 黑名单
OAuth2 access token JWT 这就是 JWT 的”亲生”场景——一次性授权
金融交易、合规要求高 Session Cookie + 服务端会话 撤销/审计/合规要求 → 服务端可控

说白了:

  • 要简单、单体应用、不用跨域:Session Cookie 永远没错
  • 跨域、跨服务、要 SSO:JWT 是合适的——别忘了配 Redis 黑名单解决撤销
  • 金融/医疗/合规场景:传统会话 + 服务端审计——别为”无状态”牺牲可控性

一句话收口

JWT 不是糟糕的技术,它只是被很多人用在了它没设计好的场景——单体应用的会话管理。

真正的工程姿势是:

  1. 明确场景——单体?跨服务?OAuth?SSO?不同场景选不同方案
  2. 如果用 JWT 做会话——配 Redis 短黑名单,用 5% 的 Redis 内存换回撤销能力
  3. payload 只塞必要字段、必走 HTTPS、敏感数据不入 JWT——三条底线

记住:好工程师不是”用什么技术”的问题,而是”知道什么时候用什么、用了之后补什么短板”。

参考资料:

文章目录
  1. 1. 先把结论摆桌面:JWT 不是不好,是用错了地方
  2. 2. JWT 是什么:30 秒看懂登录流程
  3. 3. JWT 做会话的 4 个真实约束
    1. 3.0.1. 约束 1:体积膨胀,每次请求都带上整个 token
    2. 3.0.2. 约束 2:签名只是 hygiene,不是亮点
    3. 3.0.3. 约束 3:撤销难,注销不算真正注销(最关键)
    4. 3.0.4. 约束 4:默认明文,要靠 HTTPS 兜底
  • 4. 破解撤销难题:JWT + Redis 短黑名单(最省内存)
    1. 4.0.1. 方案:Redis 黑名单 + 短 TTL(按 token 剩余有效期设)
    2. 4.0.2. 为什么这套方案省内存
    3. 4.0.3. 进阶:批量强制下线(”全量令牌作废”)
  • 5. 选型决策:什么场景用 JWT,什么场景用 Session
  • 6. 一句话收口