Post

JWT 的学习

JWT 的学习

对 JWT 这个概念有点模糊,看了个文章决定重新学习一下。


一、JWT是什么?

JWT 的全称是 JSON Web Token,它是一个开放标准(RFC 7519),定义了一种紧凑且自包含的方式,用于在各方之间安全地传输信息。这些信息会经过数字签名,因此可以被验证和信任。

JWT 的核心思想是“自包含(Self-contained)”。一个JWT本身就包含了验证用户身份所需的所有信息,服务器端无需再查询数据库或缓存来获取用户信息,从而实现了无状态(Stateless)的认证机制。

JWT的结构

一个JWT由三部分组成,由点(.)分隔,看起来像这样:xxxxx.yyyyy.zzzzz

  1. Header(头部)
    • 内容:包含两部分信息:令牌的类型(typ,通常是”JWT”)和所使用的签名算法(alg,如 HS256RS256)。
    • 格式:这是一个JSON对象,经过Base64Url编码后形成JWT的第一部分。
    • 示例{"alg": "HS256", "typ": "JWT"}
  2. Payload(载荷)
    • 内容:包含要传递的数据,也称为“声明(Claims)”。这些声明是关于实体(通常是用户)和其他数据的陈述。声明分为三类:
      • 注册声明(Registered Claims):预定义的一些声明,如 iss (签发者), exp (过期时间), sub (主题), aud (受众) ,iat(签发时间)等。这些不是强制性的,但推荐使用。
      • 公共声明(Public Claims):可以由使用者自由定义,但为了避免冲突,应在 IANA JSON Web Token Registry 中定义,或为其加上包含命名空间的URI。
      • 私有声明(Private Claims):这是在签发者和使用者之间共享的、自定义的声明,用于传递非标准化的信息,例如 user_id, role 等。
    • 格式:这也是一个JSON对象,经过Base64Url编码后形成JWT的第二部分。
    • 注意:Payload部分只是被编码,没有被加密。任何人都可以解码它来查看内容,所以不应该在Payload中存放敏感信息(如密码)。
  3. Signature(签名)
    • 目的:用于验证消息在传输过程中没有被篡改,并且(在使用私钥签名时)可以验证JWT的发送者是谁。
    • 生成方式:将编码后的Header、编码后的Payload和一个密钥(secret)使用Header中指定的算法(alg)进行签名。
    • 公式示例(使用HS256算法)HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)

      HMAC是什么?

      HMAC (Hash-based Message Authentication Code) 是一种结合了哈希函数(如SHA-256)和密钥的消息认证码。它不仅能检测数据是否被篡改(哈希函数的功能),还能确保这个哈希值是由持有特定密钥的人生成的。

    • 安全性:签名是JWT安全性的核心。只有持有密钥的服务端才能生成或验证签名。如果有人篡改了Header或Payload,由于没有密钥,他们无法重新生成有效的签名,因此篡改行为会被发现。

二、JWT设计的集中方案、优缺点及适用场景

JWT本身是一种标准,但在实际应用中,如何存储和管理它,形成了不同的设计方案。以下是三种主流的方案。

方案一:纯粹的无状态方案(Token存放于Web Storage)

这是最基础、最“纯粹”的JWT用法。

  • 流程
    1. 用户登录成功后,服务器生成一个JWT并返回给客户端。
    2. 客户端(如浏览器)将JWT存储在 localStoragesessionStorage 中。
    3. 在后续的每次API请求中,客户端通过HTTP的 Authorization 头部(通常是 Bearer <token>)将JWT发送给服务器。
    4. 服务器收到请求后,验证JWT的签名和过期时间。验证通过则处理请求。
  • 优点
    • 完全无状态:服务器不需要保存任何会话信息,每次请求都是独立的。
    • 扩展性好:非常适合微服务、负载均衡等分布式架构,因为任何一台服务器只要有密钥,就可以验证Token。
    • 实现简单:服务器端逻辑清晰,只负责签发和验证。
  • 缺点
    • 安全性风险(XSS)localStorage 可以被JavaScript访问。如果网站存在XSS(跨站脚本攻击)漏洞,攻击者可以窃取存储在其中的JWT,从而冒充用户。这是此方案最大的安全隐患。
    • Token无法主动失效:一旦签发,JWT在过期之前就一直有效。如果用户修改密码或管理员想强制用户下线,服务器无法主动使这个JWT失效。
    • 续签逻辑复杂:需要在客户端处理Token过期并引导用户重新登录或实现静默续签,逻辑相对复杂。
  • 适用场景
    • 会话生命周期非常短的应用。
    • 对XSS防护非常有信心的内部系统或后台管理系统。
    • 非浏览器环境的客户端,如移动App或服务器间调用(M2M),因为这些环境不存在XSS风险。

方案二:Cookie方案

为了解决XSS问题,可以将JWT存储在Cookie中。

  • 流程
    1. 用户登录成功后,服务器生成JWT,并通过 Set-Cookie 响应头将其设置到浏览器的Cookie中。
    2. 为了安全,Cookie应设置为 HttpOnlySecureHttpOnly 使JavaScript无法读取该Cookie,有效防止XSS攻击;Secure 保证Cookie只在HTTPS连接下传输。

      为什么HttpOnly 使JavaScript无法读取该Cookie,Secure 保证Cookie只在HTTPS连接下传输?

      这不是魔法,而是浏览器作为“规则执行者”严格遵守的协议。HttpOnly 和 Secure 是服务器通过 Set-Cookie 响应头向浏览器下达的指令。浏览器在接收到这些指令后,会改变它处理这个特定Cookie的行为。

    3. 浏览器在后续请求中会自动携带该Cookie。
    4. 服务器从Cookie中读取JWT并进行验证。
  • 优点
    • 有效防范XSSHttpOnly 属性是此方案的核心优势,大大提高了安全性。
    • 使用方便:浏览器自动处理Token的发送,前端代码更简洁。
  • 缺点
    • CSRF风险:Cookie会自动被浏览器发送,这使其容易受到CSRF(跨站请求伪造)攻击。需要配合其他策略(如 SameSite 属性、CSRF Token)来防御。
    • 跨域限制:Cookie存在同源策略限制。在前后端分离且跨域的场景下,Cookie的配置会变得非常复杂(需要处理CORS和withCredentials)。
    • Token无法主动失效:同样存在这个问题。
  • 适用场景
    • 传统的Web应用,尤其是前后端部署在同一个主域下的单页应用(SPA)。
    • 对安全性要求较高,且能有效处理CSRF问题的项目。

方案三:Refresh Token(刷新令牌)方案

这是目前最流行、最安全、最完善的方案,结合了安全性和用户体验。

  • 流程
    1. 用户登录时,服务器返回两个Token:
      • Access Token:一个生命周期很短(如15分钟)的JWT,用于访问受保护的API。
      • Refresh Token:一个生命周期很长(如7天)的普通字符串或JWT,用于获取新的Access Token。
    2. 客户端存储这两个Token。通常:
      • Access Token 存放在内存(如Vuex/Redux状态管理)或sessionStorage中,因为它生命周期短,暴露风险小。
      • Refresh Token 存放在 HttpOnly 的Cookie中,以确保安全。
    3. 客户端使用Access Token访问API。
    4. 如果Access Token过期(API返回401错误),客户端会向服务器的一个特定端点(如 /refresh_token)发送Refresh Token。
    5. 服务器验证Refresh Token的有效性(通常会在数据库或Redis中维护一个Refresh Token的白名单),如果有效,则签发一个新的Access Token返回给客户端。
    6. 客户端用新的Access Token重试刚才失败的API请求,对用户来说是无感的。
  • 优点
    • 极高的安全性:Access Token生命周期短,即使被盗,攻击窗口也小。Refresh Token被安全存储,不易泄露。
    • 可以主动失效:由于服务器端存储并管理Refresh Token,因此可以通过从数据库中删除某个Refresh Token来强制用户下线,解决了前两种方案无法主动失效的问题。
    • 良好的用户体验:用户无需在短期内频繁登录,实现了“静默续签”。
  • 缺点
    • 实现复杂度高:需要管理两种Token,增加了一个刷新Token的接口,并且服务器端需要引入状态(存储Refresh Token),破坏了纯粹的无状态性。
    • 需要处理并发请求问题:在Access Token过期的临界点,如果同时发出多个API请求,可能会导致多次调用刷新接口。需要前端进行统一的刷新处理。
  • 适用场景
    • 绝大多数现代Web应用和移动App。这是对安全性、用户体验和可扩展性有综合要求的项目的最佳实践。
    • 需要实现“强制下线”、“单点登录”等高级会话管理功能的系统。
    • 符合OAuth2.0规范的认证流程。

用户特别多的时候,是不是要维护特别多的Refresh Token呢,这里是否会有这样的问题呢?

“用户特别多的时候,需要维护特别多的 Refresh Token” 这个问题是真实存在的,但通过使用像 Redis 这样的高性能缓存数据库,它完全可以被优雅地解决,并且能够支持非常精细化的会话管理。因此,这并不会阻碍 Refresh Token 方案成为当今大规模应用认证体系的事实标准。

实际例子:常见的做法是用 Refresh Token 的哈希值或本身作为 Key,Value 存储用户信息(如 user_id)和其他元数据。

总结

方案 优点 缺点 适用场景
方案一:Web Storage 真正无状态,扩展性好,实现简单 XSS风险高,无法主动失效 内部系统,非浏览器客户端(如App)
方案二:Cookie 防XSS,使用方便 CSRF风险,跨域配置复杂,无法主动失效 同域的传统Web应用或SPA
方案三:Refresh Token 安全性极高,可主动失效,用户体验好 实现复杂,服务器非纯无状态 推荐方案,适用于绝大多数现代Web和移动应用

选择哪种方案取决于你的应用场景、安全需求和开发复杂度之间的权衡。对于大多数面向公众用户的应用,Refresh Token方案是当前公认的最佳实践

This post is licensed under CC BY 4.0 by the author.