当前位置: 首页 > news >正文

JWT 学习笔记:结构、原理与常见安全风险

前言

在 Web 开发的学习过程中,用户认证是我最早接触的功能之一。最开始做项目时,用的都是 Session 方案,觉得“登录成功后服务端存一份数据,客户端拿一个 session_id,大家认这个 id 就行”——这个逻辑很简单,也很直观。

后来项目从单体拆成了微服务,问题就来了。服务 A 的 Session,服务 B 读不到;服务扩容时,用户刚在服务器 1 登录,下一个请求就被负载均衡转发到了服务器 2,然后被告知“未登录”。为了解决这个问题,我引入了 Redis 做共享 Session,算是勉强撑住了。

直到某个项目被要求“完全不依赖外部存储做认证”时,我才开始认真了解 JWT。用了一段时间后,我逐渐理解了它的结构设计逻辑,也踩过一些坑——比如忘记校验过期时间导致令牌永久有效,以及早期把用户密码放在 Payload 里的“低级错误”。

这篇文章是我对认证方案学习过程的一个梳理,从 Session 的困境出发,延伸到 JWT 的结构原理,再到常见安全风险和实践建议,希望对正在思考认证方案的读者有一些参考价值。

第一部分:从 Session 到 JWT——为什么要关注 JWT

1.1 Session 认证的工作方式

基于 Session 的认证流程,早期项目中基本都是这个模式:

  1. 用户提交用户名和密码。
  2. 服务端验证通过后,在服务器内存中创建一个 Session 对象,存放用户信息(如用户 ID、角色)。
  3. 服务端生成一个唯一的 session_id,通过 Set-Cookie 写入浏览器。
  4. 浏览器后续的每次请求,都会在 Cookie 中自动携带这个 session_id
  5. 服务端根据 session_id 找到对应的 Session 数据,识别当前用户。

这个流程很成熟,标准库和框架都提供了完善的支持。在单体应用、用户量不大的场景下,它运行得相当稳定。

1.2 分布式场景下面临的问题

随着业务增长,我开始尝试将一个单体应用拆分为多个微服务,Session 方案逐渐暴露出一些需要解决的问题:

问题一:服务间 Session 无法共享

拆分为多个服务后,用户可能在服务 A 完成登录,但下一个请求被路由到服务 B。服务 B 的内存中没有这份 Session 数据,于是认为用户未登录。

问题二:水平扩展时 Session 同步困难

当单个服务实例需要扩容时,新启动的实例没有存量用户的 Session 数据。传统解决方案是引入 Redis 等外部存储作为共享 Session 中心,所有服务实例都从同一个 Redis 读取 Session。但这也带来了额外的依赖和维护成本,以及每次请求都需要访问 Redis 的网络开销。

问题三:跨域场景下的 Cookie 限制

在前后端分离的架构中,前端应用和后端 API 可能部署在不同的域名下。默认情况下,Cookie 不能跨域发送,需要额外配置 CORS 和 Cookie 的 SameSite 属性。虽然可以解决,但配置稍显繁琐。

问题四:服务端存储压力

当在线用户数量较多时,内存中保存的 Session 数据会占用相当数量的服务器资源。即使用 Redis,也需要为大规模 Session 数据预留足够的存储和带宽。

1.3 JWT 是什么:核心概念

JWT 全称是 JSON Web Token,是一种用于在网络应用之间安全传递信息的令牌格式。它最核心的特点是 自包含紧凑:JWT 本身包含了所有必要的用户身份信息(比如用户 ID、角色等),并且体积很小,适合在 URL 参数、HTTP Header 等场景中传输。

在认证场景中,JWT 的工作方式可以概括为:服务端验证用户身份后,生成一个 JWT 发给客户端;客户端后续请求时携带这个 JWT;服务端通过验证 JWT 的签名来确认其真实性和完整性,从而识别用户身份。整个过程服务端不需要存储会话状态,因此 JWT 是一种 无状态 的认证方案。

1.4 JWT 解决了 Session 的哪些问题

对应问题一与问题二:无需共享存储

JWT 将所有用户信息编码在令牌本身,服务端只需验证签名即可确认身份。无论请求被路由到哪个服务实例,只要该实例能拿到签名密钥,就能独立完成验证。不需要 Redis,不需要共享 Session 存储,水平扩展天然支持。

对应问题三:跨域友好

JWT 通过 HTTP Header 的 Authorization 字段传递,不依赖 Cookie。跨域场景下只需在服务端配置允许 Authorization 头即可,比 Cookie 跨域配置更为直接。

对应问题四:服务端无存储压力

服务端不保存任何会话数据,认证所需的信息都包含在 JWT 中。无论用户量多大,服务端都不需要为存储 Session 预留内存或 Redis 空间。

1.5 从 Session 到 JWT:核心变化总结

对比维度 Session JWT
状态管理 服务端存储 Session,有状态 客户端存储令牌,服务端无状态
扩展性 需要共享存储(Redis 等)支持水平扩展 天然支持水平扩展,无需共享存储
存储压力 服务端承担 Session 存储开销 服务端无存储压力
令牌大小 仅一个短 ID,体积小 包含完整信息,体积较大
即时失效 支持(删除 Session 即可) 不支持(有效期前无法撤销)
跨域支持 需额外配置 Cookie 跨域 通过 Header 传递,跨域友好

第二部分:JWT 的结构拆解

一个完整的 JWT 由三个部分组成,用 . 分隔:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

这三个部分分别是 HeaderPayloadSignature

2.1 Header(头部)

Header 是一个 JSON 对象,描述令牌的元数据:

{"alg": "HS256","typ": "JWT"
}
  • alg:签名算法,最常见的是 HS256(HMAC-SHA256)和 RS256(RSA-SHA256)
  • typ:令牌类型,固定为 JWT

这个 JSON 经过 Base64Url 编码后,就是 JWT 的第一部分。

2.2 Payload(载荷)

Payload 是 JWT 的主体,包含了需要传递的声明(Claims)。声明分为三类:

注册声明:一组预定义的推荐字段,虽然不是强制要求,但建议使用以提高互操作性。

声明 全称 含义
iss Issuer 签发者
sub Subject 主题,通常是用户 ID 或唯一标识
aud Audience 受众,标识 JWT 的目标接收方
exp Expiration Time 过期时间(Unix 时间戳)
nbf Not Before 生效时间
iat Issued At 签发时间
jti JWT ID 唯一标识,用于防重放

公共声明:为避免命名冲突,建议使用 IANA JSON Web Token Registry 注册的字段,或使用包含命名空间的标识符。

私有声明:业务自定义字段,比如角色、权限等级等。

值得特别留意的一点是:Payload 中的数据仅仅是 Base64Url 编码,并非加密。这意味着任何人都可以解码读取内容,因此 Payload 中不应该存放密码、密钥等敏感信息。

2.3 Signature(签名)

签名是 JWT 安全性的核心。它的生成方式为:

Signature = HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload),secret
)

简单理解:服务端把 Header 和 Payload 的 Base64Url 编码用 . 拼起来,再用密钥通过指定算法生成一个签名。验证时,服务端用同样的方式重新计算签名,然后与 JWT 携带的签名比对,如果一致,说明令牌没有被篡改过。

第三部分:JWT 的完整工作流程

3.1 签发与验证流程

  1. 用户登录:客户端将用户名、密码提交给认证服务。
  2. 身份验证:认证服务验证用户名和密码是否匹配。
  3. 签发 JWT:验证通过后,构建 Header 和 Payload,用密钥生成签名,将三部分拼接成完整的 JWT 返回给客户端。
  4. 客户端存储:客户端收到 JWT 后,存储在 localStorage、sessionStorage 或 Cookie 中。
  5. 请求携带:客户端在后续 API 请求的 HTTP Header 中添加 Authorization: Bearer <JWT>
  6. 服务端验证:服务端收到请求后,从 Header 中提取 JWT,验证签名是否有效,检查 exp 是否过期,确认 issaud 是否匹配。验证通过后,从 Payload 中读取用户信息,处理业务逻辑。

3.2 关于存储方式的考虑

JWT 存储在客户端,选择不同的存储方式有不同的安全考量:

存储方式 优点 风险 建议
localStorage 简单,持久化存储 易受 XSS 攻击,攻击者可通过注入脚本读取令牌 适用于不含敏感信息的场景,或配合 CSP 策略
sessionStorage 页面关闭即清除,降低泄露窗口 同样存在 XSS 风险 对敏感度一般的应用较为合适
HttpOnly Cookie 无法通过 JavaScript 读取,有效防御 XSS 攻击 受 CSRF 攻击影响,需额外防御 推荐方式,需配合 SameSite 和 CSRF Token

3.3 关于令牌撤回的思考

JWT 是无状态的,这带来一个固有难题:一旦签发,在有效期之前无法主动撤回。即使管理员封禁了某个用户,只要该用户持有的 JWT 仍在有效期内,依然可以被用于访问系统。

针对这个问题,常见的处理方式有:

  • 短有效期:将 exp 设置为较短的时间(如 15-30 分钟),缩小令牌被恶意利用的时间窗口
  • 配合 Refresh Token:用长期有效的 Refresh Token 换取短期有效的 Access Token,服务端可以撤销 Refresh Token
  • 黑名单机制:在服务端维护一个被撤销的 JWT 列表(如 Redis 缓存),验证时先检查是否在黑名单中
  • 版本号方案:在用户表中维护一个令牌版本号,每次签发 JWT 时包含该版本,修改密码或封禁用户时增加版本号,验证时检查版本是否匹配

第四部分:常见安全风险与避坑指南

4.1 alg=none 攻击

原理:JWT 规范允许将 Header 中的 alg 字段设为 none,表示“无签名”。如果服务端在验证时没有检查 alg 字段,攻击者可以构造一个 alg=none 的 JWT,删除签名部分,服务端仍然会将其视为有效令牌。

防范方法:在代码中明确指定允许的算法列表(如仅允许 HS256 和 RS256),如果收到 alg=none 或非白名单算法,直接拒绝。

4.2 密钥混淆攻击

原理:当服务端同时支持 HS256(对称加密)和 RS256(非对称加密)时,攻击者可以获取服务端的 RSA 公钥(公钥本身是公开的),然后用公钥作为 HS256 的密钥来签名一个 JWT。如果服务端验证时使用 RS256 的公钥但以 HS256 的方式处理,就有可能误以为这个 JWT 是合法签发的。

防范方法:固定算法列表,不同算法严格区分;在验证时根据 alg 字段选择对应的验证逻辑,HS256 用密钥,RS256 用公钥,不混用。

4.3 未校验 exp 字段

原理:部分 JWT 库在解码时默认不检查 exp 声明,需要开发者显式调用校验方法。如果开发者忘记调用,JWT 将永久有效,后果比较严重。

防范方法:验证 JWT 时,务必显式调用校验过期时间的方法。在 Python 的 PyJWT 库中,可以通过 options={"verify_exp": True} 启用,或调用 decode 时不传入 verify_exp=False

4.4 弱密钥

原理:HS256 依赖一个共享密钥来签名和验证。如果密钥强度不足,攻击者可以通过暴力破解或字典攻击获得密钥,从而伪造任意 JWT。

防范方法:使用密码学安全的随机数生成器产生密钥,密钥长度至少 32 字节(256 位),定期轮转密钥。

4.5 敏感信息泄露

原理:JWT 的 Payload 仅经过 Base64Url 编码,没有加密。任何拿到 JWT 的人都可以解码并读取 Payload 中的内容。如果开发者将密码、手机号、身份证号等敏感信息放入 Payload,就会造成信息泄露。

防范方法:Payload 中只存放必要的非敏感信息(如用户 ID、用户名、角色),敏感信息通过服务端查询获取,不放入 JWT。

4.6 重放攻击

原理:JWT 一旦签发,在有效期内可以被多次重复使用。如果攻击者截获了一个 JWT,就可以在令牌有效期内冒充用户发送请求。

防范方法:设置较短的过期时间(如 15 分钟),使用 jti(JWT ID)声明配合服务端缓存,记录已使用过的 jti,防止同一令牌被重复使用;始终使用 HTTPS 防止令牌在传输中被截获。

4.7 安全实践总结

实践 说明
使用 HTTPS 防止令牌在传输过程中被中间人截获
严格控制算法 限制 alg 字段为白名单算法,拒绝 none
校验过期时间 务必显式调用 exp 校验,不依赖库默认行为
强密钥 至少 32 字节,使用安全随机数生成
最小化 Payload 只存必要信息,不存敏感数据
短有效期 + Refresh Token Access Token 短有效,Refresh Token 可撤销
安全存储 优先使用 HttpOnly Cookie 存储 JWT

结语

从 Session 到 JWT,本质上是在“有状态”和“无状态”之间做权衡。Session 方案把状态放在服务端,提供了更好的控制力;JWT 方案把状态交给客户端,换来了更好的扩展性。

在实际项目中,这两种方案并不是非此即彼的对立关系。有些项目会同时使用两者——比如用 Session 管理 Web 应用的登录状态,用 JWT 作为 API 访问令牌。关键在于根据业务需求和技术约束,选择合适的方案。

http://www.jsqmd.com/news/1405718/

相关文章:

  • GitHub Desktop 汉化终极指南:用一款开源工具,5 分钟让英文界面彻底说中文
  • 2026企业招投标数据查询平台:正规服务商选型标准盘点、避坑指南及靠谱选择全解析 - 产业观察报
  • 点喷加盟品牌口碑哪里能查?四个渠道告诉你 - 资讯报道
  • Product Hunt 每日热榜 | 2026-08-14
  • AnimateLCM-SVD高清动画生成指南:4步推理产出25帧视频
  • 管家婆售后揭秘:年采购500万的“大客户”,可能是公司最大的利润黑洞
  • 用 AI 求职助手 Get Jobs 自动投简历,我把找工作变成了一件“跑数据“的事
  • Classic Shell完整上手手册:Windows开始菜单改造与效率提升实战
  • 在 Android 手机上运行完整版 VS Code:vscode_for_android 本地部署与调优指南
  • 一招告别Armoury Crate卡顿:G-Helper,不到10MB的华硕笔记本轻量控制工具
  • AI 漫剧工具横评:知漫剧一站式平台实测解析,对比即梦、小云雀、豆包
  • Python面向对象编程(OOP):从入门到精通
  • RevokeMsgPatcher防撤回补丁4步上手
  • JWT认证与授权:完整实现方案
  • 光谱点喷和普通点喷有什么区别?差在技术含金量 - 天下观知
  • 第一次去新疆旅游选包车!2026新手南北疆环线包车攻略大全 - 新疆中旅(疆途无忧)
  • 2026企业招投标商机:标讯平台性价比怎么选?全国实力服务商盘点+选型攻略+签约避坑FAQ - 行业观察网
  • 别再为缓存视频发愁:这款开源视频下载工具,一条链接搞定200+网站
  • DanbaidongRP描边系统详解:Outline/睫毛/辅助材质的一体化解决方案
  • 告别动捕设备,普通创作者如何一键完成AI动作迁移?ComfyUI-MimicMotionWrapper 上手实战
  • BiliTools完全指南:用一个30MB的轻量工具搞定B站高清视频下载
  • 2026年当下:东营仓库托盘厂家直销上门回收配送,物流老板省心省事-宏胜托盘收售 - 行业甄选汇
  • 百度库库AI月活超1亿领跑AI办公,深入金融等专业场景挑战BAT格局!
  • 洛雪音乐音源配置完整指南:三步导入免费无损音源,告别会员限制
  • 海太欧林集团简介
  • 牛肉汉堡品牌加盟哪家服务好:【美洲汉堡】筑梦护航 - 松梢月冷
  • 2026年AIGC内容产业融资回暖,资本从模型到内容,寻找“会讲故事”的公司
  • 成都哪里可以买宠物级幼犬?从健康、预算到售后一次讲清楚 - 四川同城宠物观察
  • 一文搞懂如何把屏幕秒变虚拟摄像头:Deskreen 实战全攻略
  • 傅里叶变换FFT(基于Excel)