JWT 的学习
对 JWT 这个概念有点模糊,看了个文章决定重新学习一下。
一、JWT是什么?
JWT 的全称是 JSON Web Token,它是一个开放标准(RFC 7519),定义了一种紧凑且自包含的方式,用于在各方之间安全地传输信息。这些信息会经过数字签名,因此可以被验证和信任。
JWT 的核心思想是“自包含(Self-contained)”。一个JWT本身就包含了验证用户身份所需的所有信息,服务器端无需再查询数据库或缓存来获取用户信息,从而实现了无状态(Stateless)的认证机制。
JWT的结构
一个JWT由三部分组成,由点(.)分隔,看起来像这样:xxxxx.yyyyy.zzzzz
- Header(头部)
- 内容:包含两部分信息:令牌的类型(
typ,通常是”JWT”)和所使用的签名算法(alg,如HS256或RS256)。 - 格式:这是一个JSON对象,经过Base64Url编码后形成JWT的第一部分。
- 示例:
{"alg": "HS256", "typ": "JWT"}
- 内容:包含两部分信息:令牌的类型(
- Payload(载荷)
- 内容:包含要传递的数据,也称为“声明(Claims)”。这些声明是关于实体(通常是用户)和其他数据的陈述。声明分为三类:
- 注册声明(Registered Claims):预定义的一些声明,如
iss(签发者),exp(过期时间),sub(主题),aud(受众) ,iat(签发时间)等。这些不是强制性的,但推荐使用。 - 公共声明(Public Claims):可以由使用者自由定义,但为了避免冲突,应在 IANA JSON Web Token Registry 中定义,或为其加上包含命名空间的URI。
- 私有声明(Private Claims):这是在签发者和使用者之间共享的、自定义的声明,用于传递非标准化的信息,例如
user_id,role等。
- 注册声明(Registered Claims):预定义的一些声明,如
- 格式:这也是一个JSON对象,经过Base64Url编码后形成JWT的第二部分。
- 注意:Payload部分只是被编码,没有被加密。任何人都可以解码它来查看内容,所以不应该在Payload中存放敏感信息(如密码)。
- 内容:包含要传递的数据,也称为“声明(Claims)”。这些声明是关于实体(通常是用户)和其他数据的陈述。声明分为三类:
- 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用法。
- 流程:
- 用户登录成功后,服务器生成一个JWT并返回给客户端。
- 客户端(如浏览器)将JWT存储在
localStorage或sessionStorage中。 - 在后续的每次API请求中,客户端通过HTTP的
Authorization头部(通常是Bearer <token>)将JWT发送给服务器。 - 服务器收到请求后,验证JWT的签名和过期时间。验证通过则处理请求。
- 优点:
- 完全无状态:服务器不需要保存任何会话信息,每次请求都是独立的。
- 扩展性好:非常适合微服务、负载均衡等分布式架构,因为任何一台服务器只要有密钥,就可以验证Token。
- 实现简单:服务器端逻辑清晰,只负责签发和验证。
- 缺点:
- 安全性风险(XSS):
localStorage可以被JavaScript访问。如果网站存在XSS(跨站脚本攻击)漏洞,攻击者可以窃取存储在其中的JWT,从而冒充用户。这是此方案最大的安全隐患。 - Token无法主动失效:一旦签发,JWT在过期之前就一直有效。如果用户修改密码或管理员想强制用户下线,服务器无法主动使这个JWT失效。
- 续签逻辑复杂:需要在客户端处理Token过期并引导用户重新登录或实现静默续签,逻辑相对复杂。
- 安全性风险(XSS):
- 适用场景:
- 会话生命周期非常短的应用。
- 对XSS防护非常有信心的内部系统或后台管理系统。
- 非浏览器环境的客户端,如移动App或服务器间调用(M2M),因为这些环境不存在XSS风险。
方案二:Cookie方案
为了解决XSS问题,可以将JWT存储在Cookie中。
- 流程:
- 用户登录成功后,服务器生成JWT,并通过
Set-Cookie响应头将其设置到浏览器的Cookie中。 - 为了安全,Cookie应设置为
HttpOnly和Secure。HttpOnly使JavaScript无法读取该Cookie,有效防止XSS攻击;Secure保证Cookie只在HTTPS连接下传输。为什么HttpOnly 使JavaScript无法读取该Cookie,Secure 保证Cookie只在HTTPS连接下传输?
这不是魔法,而是浏览器作为“规则执行者”严格遵守的协议。HttpOnly 和 Secure 是服务器通过 Set-Cookie 响应头向浏览器下达的指令。浏览器在接收到这些指令后,会改变它处理这个特定Cookie的行为。
- 浏览器在后续请求中会自动携带该Cookie。
- 服务器从Cookie中读取JWT并进行验证。
- 用户登录成功后,服务器生成JWT,并通过
- 优点:
- 有效防范XSS:
HttpOnly属性是此方案的核心优势,大大提高了安全性。 - 使用方便:浏览器自动处理Token的发送,前端代码更简洁。
- 有效防范XSS:
- 缺点:
- CSRF风险:Cookie会自动被浏览器发送,这使其容易受到CSRF(跨站请求伪造)攻击。需要配合其他策略(如
SameSite属性、CSRF Token)来防御。 - 跨域限制:Cookie存在同源策略限制。在前后端分离且跨域的场景下,Cookie的配置会变得非常复杂(需要处理CORS和
withCredentials)。 - Token无法主动失效:同样存在这个问题。
- CSRF风险:Cookie会自动被浏览器发送,这使其容易受到CSRF(跨站请求伪造)攻击。需要配合其他策略(如
- 适用场景:
- 传统的Web应用,尤其是前后端部署在同一个主域下的单页应用(SPA)。
- 对安全性要求较高,且能有效处理CSRF问题的项目。
方案三:Refresh Token(刷新令牌)方案
这是目前最流行、最安全、最完善的方案,结合了安全性和用户体验。
- 流程:
- 用户登录时,服务器返回两个Token:
- Access Token:一个生命周期很短(如15分钟)的JWT,用于访问受保护的API。
- Refresh Token:一个生命周期很长(如7天)的普通字符串或JWT,用于获取新的Access Token。
- 客户端存储这两个Token。通常:
- Access Token 存放在内存(如Vuex/Redux状态管理)或
sessionStorage中,因为它生命周期短,暴露风险小。 - Refresh Token 存放在
HttpOnly的Cookie中,以确保安全。
- Access Token 存放在内存(如Vuex/Redux状态管理)或
- 客户端使用Access Token访问API。
- 如果Access Token过期(API返回401错误),客户端会向服务器的一个特定端点(如
/refresh_token)发送Refresh Token。 - 服务器验证Refresh Token的有效性(通常会在数据库或Redis中维护一个Refresh Token的白名单),如果有效,则签发一个新的Access Token返回给客户端。
- 客户端用新的Access Token重试刚才失败的API请求,对用户来说是无感的。
- 用户登录时,服务器返回两个Token:
- 优点:
- 极高的安全性: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方案是当前公认的最佳实践。