JSON Web Token (JWT) 已经成为现代应用身份认证与授权机制中不可或缺的一部分。它以其轻量、无状态、可跨域等特性,在微服务、前后端分离以及移动应用等架构中大放异彩。本文将围绕JWT令牌,深入探讨其核心概念、实际应用场景、操作流程以及安全性考量,为您揭示这个强大工具的方方面面。
一、JWT令牌:它究竟“是什么”?
要理解JWT,首先要明确它的基本构成和核心概念。
1.1 基本概念
JWT(JSON Web Token)是一种紧凑且自包含的方式,用于在各方之间安全地传输信息。这些信息可以被验证和信任,因为它们经过了数字签名。一个JWT通常由三部分组成,并用点号(.)分隔开:
- Header(头部)
- Payload(载荷)
- Signature(签名)
例如,一个JWT看起来会是这样:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
虽然JWT是经过编码的,但它并不是加密的。这意味着任何人都可以解码它来读取其中的内容(载荷),但无法修改它,因为任何修改都会导致签名验证失败。
1.2 结构构成
JWT的三个部分分别承担不同的作用:
-
头部(Header)
头部通常包含两部分信息:
alg(algorithm):指定签名算法,例如HMAC SHA256 (HS256) 或 RSA (RS256)。typ(type):指定令牌类型,通常为 “JWT”。
这是一个头部的JSON示例:
{ "alg": "HS256", "typ": "JWT" }这个JSON对象随后会经过Base64 Url编码,构成JWT的第一部分。
-
载荷(Payload)
载荷是JWT的核心,包含实际要传输的数据,也称为“声明”(Claims)。声明分为三种类型:
-
注册声明(Registered Claims): 预定义的一些声明,非强制使用,但强烈建议使用以提供互操作性。常见的有:
iss(issuer):签发者exp(expiration time):过期时间戳sub(subject):主题(通常是用户ID)aud(audience):接收者iat(issued at):签发时间jti(JWT ID):JWT的唯一标识符
- 公共声明(Public Claims): 可以由使用JWT的人自行定义,但为了避免冲突,建议在IANA JSON Web Token Registry中注册,或者使用包含命名空间的URI。
-
私有声明(Private Claims): 用于在同意使用JWT的各方之间共享信息,是自定义的声明,既不注册也不公共。例如,可以包含用户的角色(
"role": "admin")或部门信息。
这是一个载荷的JSON示例:
{ "sub": "1234567890", "name": "John Doe", "iat": 1516239022, "role": "user" }这个JSON对象同样会经过Base64 Url编码,构成JWT的第二部分。
-
注册声明(Registered Claims): 预定义的一些声明,非强制使用,但强烈建议使用以提供互操作性。常见的有:
-
签名(Signature)
签名是JWT的防篡改机制。它的生成过程如下:
- 将Base64 Url编码后的头部。
- 将Base64 Url编码后的载荷。
- 使用头部中指定的算法(例如HMAC SHA256)和服务器的密钥(secret)对前两部分进行签名。
签名的计算公式通常是:
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)签名确保了JWT的完整性。当服务器收到JWT时,它会使用相同的密钥和算法重新计算签名。如果计算出的签名与JWT中的签名不匹配,则表明JWT在传输过程中被篡改,或者签发者不是预期的服务器,令牌将视为无效。
二、为何选择JWT:它“为什么”如此受欢迎?
JWT之所以广泛流行,得益于其独特的优势,尤其是在现代分布式系统和前后端分离架构中。
2.1 无状态性与可扩展性
传统的Session认证需要服务器维护用户会话状态,将Session ID存储在Cookie中,并在服务器端存储Session数据。这在单体应用中尚可,但在分布式系统或微服务架构下,Session共享和同步会变得非常复杂且难以扩展。
JWT则不同。由于其自包含(Self-contained)的特性,所有必要的用户信息和认证状态都封装在令牌本身。服务器收到JWT后,只需验证其签名和过期时间即可,无需查询数据库或共享Session存储。这种无状态(Stateless)的特性使得:
- 水平扩展变得简单: 任何服务器实例都可以验证JWT,无需担心会话粘性或Session同步问题。这极大地提高了系统的可扩展性。
- 降低服务器资源消耗: 无需在服务器端存储大量的会话数据,减轻了数据库或缓存的压力。
2.2 跨域与微服务支持
在前后端分离的架构中,前端(如Vue/React SPA应用)与后端API可能部署在不同的域上,或者后端采用微服务架构。JWT天然支持这些场景:
- 跨域认证: JWT通常通过HTTP请求头(
Authorization: Bearer <token>)发送,不受同源策略的限制,完美支持跨域请求。 - 微服务间的认证: 当用户请求经过网关转发到不同的微服务时,JWT可以作为统一的认证凭证在服务间传递。每个微服务都能够独立验证JWT,无需再向认证中心发起请求,简化了服务间的认证流程,提高了效率。
2.3 安全性特点
JWT通过数字签名提供了重要的安全保障:
- 防篡改: 签名机制确保了JWT在传输过程中没有被恶意修改。任何对头部或载荷的改动都会导致签名验证失败,从而拒绝该令牌。
- 数据完整性: 签名保证了令牌内容的完整性,接收方可以信任令牌中的信息确实来自签发者。
- 非对称加密支持: 除了HMAC (HS256) 这种对称加密算法外,JWT也支持RSA (RS256) 等非对称加密算法。这意味着签发方使用私钥签名,而验证方使用公钥验证,进一步增强了安全性,特别适用于第三方集成或认证中心与多个服务提供商之间的场景。
然而,需要强调的是,JWT本身不对载荷进行加密,所以敏感信息不应直接放在载荷中。
三、JWT令牌的踪迹:它“在哪里”被使用?
JWT在认证流程中扮演核心角色,涉及客户端的存储、网络的传输以及服务器端的验证。
3.1 客户端存储位置
JWT被签发给客户端后,客户端需要将其存储起来以便在后续请求中使用。常见的存储位置有:
-
LocalStorage (本地存储):
- 优点: 持久化存储,即使浏览器关闭后也不会丢失,适合长期有效的令牌。容量较大(通常5MB+)。
- 缺点: 易受XSS(跨站脚本攻击)影响。如果攻击者注入恶意脚本,可以轻松读取并窃取存储在LocalStorage中的JWT。
重要提示: 鉴于XSS的风险,不建议将JWT直接存储在LocalStorage中作为主要的认证凭证,尤其对于高安全要求的应用。
-
SessionStorage (会话存储):
- 优点: 仅在当前会话(浏览器标签页)有效,标签页关闭后自动清除。可以避免一些长期令牌被盗用的风险。
- 缺点: 仍受XSS影响。每次用户关闭再打开浏览器都需要重新登录。
-
HTTP Only Cookie:
- 优点: 设置
HttpOnly属性后,JavaScript无法直接访问Cookie内容,可以有效防御XSS攻击。同时可以设置Secure属性确保只在HTTPS下传输,并设置SameSite属性防止CSRF攻击。 - 缺点: 受到同源策略限制,默认情况下不支持跨域。通常需要配置CORS或者使用子域共享Cookie。大小有限(通常4KB)。
- 优点: 设置
- 内存存储: 仅在应用的JavaScript变量中存储,当页面刷新或应用关闭时丢失。适用于对安全性要求极高,或者令牌有效期极短的场景。
最佳实践: 通常建议将短寿命的Access Token存储在内存中(配合HTTP Only Cookie存储长寿命的Refresh Token),或者直接使用HTTP Only Cookie来存储JWT,并结合合理的过期策略来提高安全性。
3.2 传输方式
客户端获取到JWT后,通常通过以下方式将其发送给服务器:
-
HTTP Authorization Header (推荐方式):
最常见且推荐的方式。将JWT放置在HTTP请求头的
Authorization字段中,格式为Bearer <token>。例如:Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...这种方式简单、标准,并且不依赖于Cookie,因此可以轻松实现跨域请求。
-
请求参数 (URL Query Parameter):
将JWT作为URL查询参数发送。例如:
GET /api/data?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...不推荐此方式: JWT会暴露在URL中,可能被服务器日志、浏览器历史记录等记录下来,增加被窃取的风险。此外,URL长度也有限制。
-
请求体 (Request Body):
将JWT放置在POST请求的请求体中。这种方式较为少见,通常不作为主流认证方式,因为它需要特定的解析逻辑,且不如HTTP头方便。
3.3 应用场景
JWT被广泛应用于各种现代应用架构:
- 单页应用(SPAs)和移动应用: 完美支持前后端分离架构,前端无需依赖Cookie管理会话,直接通过HTTP头携带JWT与后端API交互。
- API认证: 许多公共或内部API使用JWT进行客户端认证和授权。
- 微服务架构: JWT作为统一的认证凭证,可以在多个微服务之间安全、高效地传递用户身份信息,避免了复杂的跨服务会话管理。
- 第三方授权: OAuth 2.0中可以使用JWT作为授权码(Authorization Code)或访问令牌(Access Token),实现第三方应用对用户资源的访问授权。
- Webhooks: 可以使用JWT对Webhooks发送的数据进行签名,确保数据的完整性和真实性。
四、管理JWT令牌:它的“生命周期与考量”?
JWT的有效管理,特别是其有效期和大小,直接影响系统的安全性与性能。
4.1 有效时长(过期时间 exp)
JWT的过期时间(exp声明)是其非常重要的安全特性。合理设置过期时间是关键:
-
访问令牌(Access Token)的有效期:
应该设置得相对短(例如5分钟到1小时)。短寿命可以:
- 减少令牌被盗用后造成的损害。
- 即使令牌被窃取,其有效时间也有限,降低攻击窗口。
-
刷新令牌(Refresh Token)的有效期:
通常与访问令牌配合使用,其有效期可以设置得相对长(例如几天、几周甚至几个月)。
- 当访问令牌过期时,客户端可以使用刷新令牌向认证服务器请求新的访问令牌。
- 刷新令牌通常只在认证服务器的特定端点上使用,且应严格管理,甚至绑定IP地址或设备指纹。
这种“短命的访问令牌 + 长命的刷新令牌”组合是推荐的实践,兼顾了安全性和用户体验。
4.2 令牌大小
JWT的载荷中包含的数据越多,令牌的整体大小就越大。虽然理论上JWT大小没有严格限制,但实际应用中需要考虑:
- 传输效率: JWT会随着每个HTTP请求传输,较大的令牌会增加网络负载,影响性能,尤其是在移动网络环境下。
- HTTP头限制: 某些Web服务器或代理对HTTP请求头的大小有限制。过大的JWT可能导致请求被拒绝。
建议: JWT的载荷应仅包含必要的、非敏感的用户身份或授权信息。尽量避免在其中包含大量冗余数据或敏感信息。
4.3 访问与刷新令牌的数量考量
在一个典型的认证授权流程中,通常涉及两种JWT:
-
一个访问令牌(Access Token):
用于访问受保护的资源。有效期短,每次请求资源时携带。
-
一个刷新令牌(Refresh Token):
用于在访问令牌过期后,向认证服务器请求新的访问令牌。有效期长,仅在需要刷新时使用。刷新令牌本身不用于访问资源,仅用于获取新的访问令牌。
这种双令牌策略提高了系统的安全性。即使访问令牌被窃取,其短暂的生命周期也限制了攻击的范围。而刷新令牌通常存储在更安全的位置(例如HTTP Only Cookie),且只在特定场景下使用。
五、JWT令牌的“如何”运作:从创建到验证
理解JWT的工作流程是实现其应用的关键。
5.1 生成与签发
- 用户登录: 用户在客户端(浏览器或App)输入凭证(用户名/密码)并发送到认证服务器。
- 凭证验证: 认证服务器验证用户凭证的合法性。
-
生成JWT:
- 如果凭证有效,认证服务器会构建JWT的头部和载荷。
- 头部包含算法信息,载荷包含用户的身份标识(如用户ID)、角色、过期时间等信息。
- 服务器使用预设的密钥(Secret Key)对头部和载荷进行签名,生成最终的JWT。
- 签发JWT: 认证服务器将生成的JWT(通常是访问令牌和刷新令牌)作为响应体的一部分发送回客户端。
5.2 客户端使用
- 存储令牌: 客户端收到JWT后,根据安全策略将其存储在LocalStorage、SessionStorage或HTTP Only Cookie中。
-
附加令牌: 在后续需要访问受保护资源的请求中,客户端会从存储中取出JWT,并将其附加到HTTP请求的
Authorization头中(Bearer <token>)。 - 发送请求: 客户端将包含JWT的请求发送到资源服务器。
5.3 服务端验证
资源服务器收到客户端请求后,会执行以下验证步骤:
-
提取JWT: 从
Authorization头中提取出JWT。 -
验证签名:
- 服务器使用与签发时相同的密钥(或公钥,如果使用非对称加密)对JWT的头部和载荷重新计算签名。
- 将计算出的签名与JWT中自带的签名进行比对。如果两者不一致,则认为令牌被篡改,验证失败,拒绝请求。
-
解析载荷与验证声明:
- 如果签名验证通过,服务器会解码JWT的载荷,获取其中的声明(例如用户ID、角色、过期时间等)。
- 检查过期时间(
exp): 验证令牌是否已过期。如果过期,拒绝请求并提示客户端刷新令牌。 - 检查其他声明: 根据业务需求,可能还会检查
iss(签发者)、aud(接收者)等声明,确保令牌的来源和用途符合预期。
- 授权访问: 如果所有验证通过,服务器认为该请求是合法的,可以根据JWT载荷中的信息(如用户角色、权限)决定是否允许访问请求的资源。
5.4 刷新机制
当访问令牌过期时,客户端需要刷新令牌以获取新的访问权限:
- 访问令牌过期: 客户端向资源服务器发起请求,但由于访问令牌过期而被拒绝(通常返回401 Unauthorized)。
- 使用刷新令牌: 客户端检测到访问令牌过期,会使用之前存储的刷新令牌向认证服务器的特定刷新端点发起请求。
- 验证刷新令牌: 认证服务器验证刷新令牌的合法性、是否过期、是否被吊销等。
- 签发新令牌: 如果刷新令牌有效,认证服务器会签发一个新的访问令牌(可能也会签发新的刷新令牌),并返回给客户端。
- 更新令牌: 客户端更新其存储的访问令牌(和刷新令牌),然后使用新的访问令牌重新发起之前的资源请求。
5.5 注销与吊销
由于JWT的无状态特性,传统的“注销会话”概念不再适用。因为服务器不存储JWT状态,所以无法直接“删除”一个已签发且未过期的JWT。为了实现注销和令牌吊销,通常有以下策略:
- 客户端注销: 最简单的注销方式是客户端删除其本地存储的所有JWT(访问令牌和刷新令牌)。这使得客户端无法再发送有效的请求,但已签发的JWT在过期前仍然有效,只是客户端无法再使用。
-
黑名单(Blacklisting): 服务器维护一个已吊销的JWT列表(通常存储在Redis等高性能缓存中),每次收到JWT时,除了验证签名和过期时间,还会查询黑名单。如果JWT在黑名单中,则拒绝请求。
- 适用场景: 强制用户退出、密码重置后旧令牌失效、发现令牌泄露时强制吊销。
- 挑战: 需要额外的存储和查询开销,且黑名单列表的同步和管理可能复杂。对于短寿命的令牌,其效益可能不高,因为它们很快就会自行过期。
- 强制过期/缩短寿命: 通过缩短访问令牌的有效期,减少其被盗用后的攻击窗口。结合刷新令牌机制,可以提供良好的用户体验,同时降低安全风险。
- 撤销刷新令牌: 当用户更改密码、账户被冻结或管理员强制用户下线时,可以直接使刷新令牌失效。因为刷新令牌是获取新访问令牌的唯一途径,其失效将迫使客户端重新登录。
六、应对挑战:如何“更安全、更健壮地”使用JWT?
虽然JWT提供了强大的认证和授权机制,但并非没有安全挑战。以下是一些常见的问题及对应的最佳实践。
6.1 常见安全挑战与对策
-
令牌泄露(Token Theft):
- 风险: JWT被攻击者窃取后,攻击者可以冒充合法用户发起请求。
- 对策:
- 始终使用HTTPS/SSL加密所有通信,防止中间人攻击(MITM)。
- 将刷新令牌存储在HttpOnly Cookie中,以防止XSS攻击窃取。
- 访问令牌存储在内存中或使用短有效期。
- 考虑将JWT绑定到用户IP地址或设备指纹,但会增加复杂性。
-
跨站脚本攻击(XSS):
- 风险: 攻击者在页面注入恶意JavaScript,窃取存储在LocalStorage或SessionStorage中的JWT。
- 对策:
- 严格验证和净化用户输入,避免XSS漏洞。
- 将敏感的刷新令牌存储在HttpOnly Cookie中,JavaScript无法访问。
- 对于访问令牌,如果存储在客户端可访问的地方,则必须加强XSS防御。
-
跨站请求伪造(CSRF):
- 风险: 攻击者诱导用户点击恶意链接,利用用户已登录的会话发送非用户意愿的请求。当JWT存储在Cookie中时,面临CSRF风险。
- 对策:
- 对包含JWT的Cookie设置
SameSite=Strict或Lax属性,阻止跨站请求携带Cookie。 - 在请求中加入CSRF Token(如同步令牌模式),后端验证该令牌。
- 对包含JWT的Cookie设置
-
密钥管理不当:
- 风险: JWT的签名密钥一旦泄露,攻击者可以伪造任意有效的JWT。
- 对策:
- 将密钥视为最高机密,妥善保管,绝不能硬编码在代码中或提交到版本控制系统。
- 使用环境变量、密钥管理服务(如Vault、AWS KMS、Azure Key Vault)或硬件安全模块(HSM)存储密钥。
- 定期轮换密钥。
- 如果使用非对称加密(RSA),确保私钥安全,公钥可以公开。
-
重放攻击(Replay Attack):
- 风险: 攻击者截获有效的JWT后,在短时间内多次发送相同请求。
- 对策:
- 设置短的访问令牌有效期。
- 在JWT中加入唯一标识符(
jti声明),并维护一个使用过的jti的缓存,防止重放(通常用于刷新令牌或一次性令牌)。
6.2 最佳实践总结
- 始终使用HTTPS: 这是最基本的安全措施,确保令牌在传输过程中不被窃听。
- 合理设置令牌有效期: 采用“短寿命访问令牌 + 长寿命刷新令牌”的双令牌策略。
-
妥善存储刷新令牌: 将刷新令牌存储在HttpOnly Cookie中,并设置
Secure和SameSite属性。访问令牌可以存储在内存中。 - 保护签名密钥: 将密钥视为敏感信息,使用安全的密钥管理方案,并定期轮换。
- 载荷不含敏感信息: JWT载荷是可解码的,避免在其中放置用户的密码、银行卡号等高度敏感信息。只存放必要的身份标识和授权信息。
- 强制过期/黑名单机制: 在需要立即注销用户或吊销令牌时,实现黑名单机制或强制刷新令牌失效。
-
验证所有声明: 服务器端不仅要验证签名,还要仔细检查
exp、nbf(not before time)、iss、aud等所有相关声明。 - 防御XSS和CSRF: 采取通用Web安全措施,如输入验证、CSP(内容安全策略)、CSRF Token等。
通过深入理解JWT的内部机制、应用场景、生命周期管理以及安全考量,开发者可以更自信、更安全地将其集成到自己的应用中,构建出健壮且高性能的认证与授权系统。