JSON Web Token (JWT) 是一种轻量级、自包含的标准,用于在各方之间安全地传输信息。它以JSON对象的形式承载声明(claims),并使用数字签名进行保护,确保其内容的完整性和真实性。JWT的核心设计理念是“自包含”,即令牌本身包含了验证所需的所有信息,无需频繁查询数据库或共享会话状态。
【jwt是什么】— 解构JSON Web Token的核心构成与本质
要理解JWT,首先要从其结构入手。一个完整的JWT由三个通过点(.)连接的部分组成:
- 头部 (Header)
- 载荷 (Payload)
- 签名 (Signature)
这三个部分都经过Base64Url编码,最终形成一个紧凑、URL安全的字符串。重要的是,JWT不是加密的,而是编码和签名的。这意味着任何人都可以解码它来读取其中的内容,但无法在没有正确密钥的情况下修改它而不被发现。
头部 (Header)
头部通常包含两部分信息:
typ(Type): 表示令牌的类型,固定为JWT。alg(Algorithm): 表示用于签名的算法,例如HMAC SHA256 (HS256) 或 RSA SHA256 (RS256)。
示例头部:
{ "alg": "HS256", "typ": "JWT" }Base64Url编码后,它会是类似
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9的字符串。
载荷 (Payload / Claims)
载荷是JWT的实际信息载体,其中包含了一组声明 (claims)。这些声明是关于实体(通常是用户)或其他数据的陈述。声明分为三类:
-
注册声明 (Registered Claims): 这些是预定义的一些声明,推荐使用但非强制。
iss(Issuer): 签发人。sub(Subject): 主题。aud(Audience): 接收该JWT的一方。exp(Expiration Time): JWT的过期时间戳。nbf(Not Before): JWT在此时间之前是无效的。iat(Issued At): JWT的签发时间。jti(JWT ID): JWT的唯一标识。
- 公开声明 (Public Claims): 这些可以在JWT中自由定义,但为了避免冲突,建议使用IANA JSON Web Token Registry或以URI形式定义。
- 私有声明 (Private Claims): 这些是自定义的声明,用于在JWT的使用方和签发方之间共享特定信息。例如,用户ID、角色等。
示例载荷:
{ "sub": "1234567890", "name": "张三", "admin": true, "iat": 1516239022, "exp": 1516242622 }Base64Url编码后,它会是类似
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ilx1NTU2Nlx1NWMzMyIsImFkbWluIjp0cnVlLCJpYXQiOjE1MTYyMzkwMjIsImV4cCI6MTUxNjI0MjYyMn0的字符串。
签名 (Signature)
签名是JWT安全的核心。它通过对编码后的头部和编码后的载荷,以及一个只有服务器知道的密钥(secret),使用头部中指定的算法进行哈希运算生成。签名的目的是验证JWT的发送者,并确保消息在传输过程中没有被篡改。如果签名验证失败,则表明令牌已被修改或不是由原始签发者签发的。
签名计算方式(以HS256为例):
HMACSHA256( Base64Url(header) + "." + Base64Url(payload), secret)
最终,这三个部分通过点连接起来,形成一个完整的JWT字符串。
【为什么】— JWT解决了哪些痛点?相较于传统会话有哪些优势?
JWT之所以受到广泛欢迎,是因为它有效地解决了传统基于会话(Session-Cookie)的认证方式在现代分布式系统和API场景中的一些痛点:
-
无状态性 (Statelessness):
传统会话管理需要在服务器端存储用户会话信息,例如将用户登录状态保存在服务器内存或数据库中。这使得服务器变得有状态,在部署多个服务器或进行负载均衡时,需要实现会话共享或会话粘滞,增加了复杂性。
JWT的“自包含”特性意味着服务器无需存储任何会话信息。令牌本身包含了用户身份和权限,每次请求时客户端携带JWT,服务器只需验证其签名和过期时间即可。这大大简化了服务器端的会话管理,使得系统更易于扩展和维护。
-
可扩展性 (Scalability):
由于无状态性,JWT天然地支持水平扩展。无论有多少台服务器实例,它们都可以独立地验证同一个JWT,无需同步会话状态。这对于微服务架构和云原生应用尤其有利,使得后端服务可以根据流量弹性伸缩。
-
跨域与跨平台兼容性 (Cross-Domain & Cross-Platform Compatibility):
传统的Cookie通常受到同源策略的限制,在跨域场景下传递和使用比较复杂。而JWT作为一种令牌,可以通过HTTP请求头(如
Authorization: Bearer)方便地在不同域之间传递。这使得它非常适合于前后端分离的架构(如单页应用SPA、移动应用)以及需要与第三方API集成的场景。 -
移动端和API认证友好:
移动应用通常不依赖浏览器的Cookie机制,而JWT作为一种通用的令牌标准,可以轻松地在移动应用中集成和使用,用于认证和授权对后端API的访问。
-
减少数据库查询:
在每次请求中,服务器通常无需查询数据库来获取用户的身份信息或权限。所需的一切都在JWT的载荷中。这减少了数据库负载,提升了响应速度。
-
单点登录 (Single Sign-On, SSO):
JWT可以作为实现SSO的一种有效机制。在一个身份提供者签发JWT后,用户可以携带该令牌访问多个不同的服务提供者,而无需在每个服务中重新登录。
简而言之,JWT解决了传统会话模式中“服务器状态管理”的负担,实现了真正意义上的无状态认证,从而提升了系统的可伸缩性、灵活性和跨平台能力。
【哪里】— JWT通常应用于哪些场景和流程?
JWT因其自包含、无状态的特性,在现代Web应用和分布式系统中有着广泛的应用:
-
用户认证 (Authentication):
这是JWT最常见的应用场景。当用户成功登录后,服务器会生成一个JWT并返回给客户端。客户端在后续的每次请求中都会携带这个JWT(通常放在HTTP请求头的
Authorization字段中,格式为Bearer),服务器接收到请求后,通过验证JWT的签名和有效性来确认用户身份,而无需查询数据库或会话存储。 -
用户授权 (Authorization):
JWT的载荷中可以包含用户的角色、权限等信息(例如
"roles": ["admin", "editor"])。服务器在接收到JWT并验证其有效性后,可以直接从载荷中提取这些授权信息,从而判断用户是否有权访问特定资源或执行特定操作,进一步减少了数据库查询。 -
信息交换 (Information Exchange):
由于JWT是经过签名的,它可以确保信息的完整性和来源的真实性。因此,JWT可以用于在不同服务之间安全地传输某些声明,例如在微服务架构中,一个服务可以将一些用户上下文信息打包到JWT中传递给另一个服务,而不需要额外的安全通道。
-
单点登录 (Single Sign-On, SSO):
在一个大型企业或产品生态系统中,用户可能需要访问多个相互独立的应用程序。通过JWT,用户只需在一个身份提供者(IdP)处登录一次,IdP签发一个JWT,用户便可携带此JWT访问所有受信任的服务提供者(SP),实现了无缝的单点登录体验。
-
前后端分离应用 (SPA, Mobile App) 的API安全:
对于单页应用(SPA)和移动应用,后端通常只提供API接口。JWT是API认证的理想选择,因为它不依赖于Cookie,可以轻松地通过HTTP请求头传递,解决了跨域问题,并提供了无状态的认证机制。
-
第三方应用授权 (OAuth 2.0):
虽然OAuth 2.0本身不是JWT,但它经常使用JWT作为Access Token或ID Token的格式。特别是OpenID Connect(基于OAuth 2.0的身份层)就明确规定了ID Token必须是JWT格式,用于客户端获取用户的身份信息。
总结: JWT的应用场景集中在需要无状态认证、跨域通信、分布式系统以及前后端分离架构中的身份验证和授权。它将用户身份和权限信息“打包”进令牌本身,使得服务器端的认证逻辑更加简洁高效。
【多少】— JWT的容量、生命周期与算法选择
关于JWT的“量”,我们主要关注以下几个方面:
-
JWT通常有多大?能存储多少信息?
JWT的大小取决于载荷中包含的声明数量和长度。由于JWT通常通过HTTP请求头传输,而大多数Web服务器和代理对HTTP请求头的大小有限制(例如几KB到几十KB不等),因此JWT不适合存储大量数据。它应该只包含必要的、非敏感的用户身份、权限和会话信息。
一个包含少量标准声明和几个自定义声明的JWT通常在几百字节到1KB左右。过度膨胀的JWT可能会导致网络延迟增加,并可能触发HTTP请求头的尺寸限制,导致请求失败。
-
JWT的生命周期是多长?
JWT的生命周期由其载荷中的
exp(Expiration Time)声明决定。这是一个Unix时间戳,表示JWT的过期时间。- 访问令牌 (Access Token): 通常建议设置较短的生命周期,例如15分钟到1小时。这是为了降低令牌被盗用后造成的风险。即使令牌被窃取,其有效时间也有限。
- 刷新令牌 (Refresh Token): 为了提升用户体验,通常会搭配一个生命周期较长的刷新令牌。当访问令牌过期时,客户端可以使用刷新令牌向认证服务器请求新的访问令牌。刷新令牌通常只在认证服务器验证,且具有更严格的存储和使用限制。
通过短生命周期的访问令牌和长生命周期的刷新令牌组合,可以在保证安全性的同时兼顾用户体验。
-
JWT有多少种签名算法?如何选择?
JWT规范支持多种签名算法,主要分为两类:
-
对称加密算法 (Symmetric Algorithms):
- 代表:HMAC (Hash-based Message Authentication Code),如HS256 (HMAC using SHA-256)。
- 特点: 使用相同的密钥进行签名和验证。实现简单,性能高。
- 适用场景: 当JWT的签发方和验证方是同一个服务,或者相互之间可以安全地共享密钥时。
-
非对称加密算法 (Asymmetric Algorithms):
- 代表:RSA (Rivest–Shamir–Adleman),如RS256 (RSA using SHA-256);ECDSA (Elliptic Curve Digital Signature Algorithm),如ES256 (ECDSA using P-256 and SHA-256)。
- 特点: 使用私钥进行签名,公钥进行验证。公钥可以公开,无需共享私钥。
- 适用场景: 当JWT的签发方和验证方是不同的服务,且无法安全地共享密钥时(例如,第三方身份提供者签发JWT,多个服务验证)。公码的公开性使得这种方式非常适合分布式系统和SSO。
选择建议:
- 如果你的应用是单体架构,或后端服务之间共享密钥安全可控,HS256通常足够且性能更优。
- 如果你的应用是微服务架构,或者需要与第三方(如OpenID Connect提供商)集成,RS256或ES256(通常更安全)是更合适的选择,因为它们允许各个服务独立验证JWT,而无需暴露签名私钥。
-
对称加密算法 (Symmetric Algorithms):
关键点: JWT应保持紧凑,生命周期可控(结合刷新令牌),并根据应用场景选择合适的签名算法以确保安全和性能。
【如何】— JWT的生成、验证与使用流程
理解JWT的工作原理,关键在于掌握其生成、验证和在客户端与服务器之间如何传递和使用。
如何生成一个JWT?
生成JWT通常是一个服务器端的任务。以下是其基本步骤:
-
定义头部 (Header): 创建一个JSON对象,指定
typ为"JWT",以及签名算法alg(例如"HS256"或"RS256")。 -
定义载荷 (Payload): 创建一个JSON对象,包含你想要在JWT中传递的声明(例如用户ID、角色、过期时间
exp、签发时间iat等)。确保不包含敏感信息。 -
Base64Url编码头部和载荷: 将头部JSON和载荷JSON分别转换为UTF-8字节流,然后进行Base64Url编码。这种编码方式是URL安全的,不会包含
+,/,=等字符。 -
生成签名:
- 将编码后的头部和编码后的载荷用点(.)连接起来,形成一个字符串:
EncodedHeader.EncodedPayload。 - 使用头部中指定的算法(例如HMAC SHA256或RSA SHA256)和服务器端的密钥(Secret Key / Private Key) 对这个字符串进行签名。
- 对生成的签名进行Base64Url编码。
- 将编码后的头部和编码后的载荷用点(.)连接起来,形成一个字符串:
-
组合成最终JWT: 将编码后的头部、编码后的载荷、编码后的签名用点(.)连接起来,形成最终的JWT字符串:
EncodedHeader.EncodedPayload.EncodedSignature。
生成流程简化:
1. 构建
Header(JSON)
2. 构建Payload(JSON)
3.EncodedHeader = Base64UrlEncode(Header)
4.EncodedPayload = Base64UrlEncode(Payload)
5.Signature = Algorithm(EncodedHeader + "." + EncodedPayload, SecretKey)
6.EncodedSignature = Base64UrlEncode(Signature)
7.JWT = EncodedHeader + "." + EncodedPayload + "." + EncodedSignature
如何验证一个JWT?
验证JWT通常也是一个服务器端的任务。以下是其基本步骤:
-
接收JWT: 从客户端的HTTP请求中获取JWT字符串(通常在
Authorization请求头中)。 - 拆分JWT: 将JWT字符串按点(.)拆分为三部分:编码的头部、编码的载荷和编码的签名。
- 解码头部和载荷: 对编码的头部和载荷进行Base64Url解码,获取原始的JSON对象。
-
验证签名(核心步骤):
- 根据解码后的头部信息,获取所使用的签名算法。
- 使用相同的算法和服务器端的密钥(Secret Key / Public Key),对编码后的头部和编码后的载荷(即第一步和第二步拆分出来的部分)进行重新签名。
- 将新生成的签名与JWT中提供的编码签名进行比对。如果两者不一致,则说明JWT已被篡改或不是由信任的签发者签发的,应立即拒绝该请求。
-
验证声明 (Claims):
- 过期时间 (
exp): 检查当前时间是否晚于exp字段指定的时间。如果已过期,则JWT无效。 - 签发人 (
iss): 验证JWT的签发人是否是你信任的来源。 - 受众 (
aud): 验证该JWT是否是签发给当前服务的。 - 未生效时间 (
nbf): 检查当前时间是否早于nbf字段指定的时间。 - 其他自定义业务逻辑验证。
- 过期时间 (
- 通过验证: 如果以上所有步骤都成功,则JWT是有效的,服务器可以信任其中的载荷信息,并根据这些信息进行后续的业务处理(如授权)。
验证流程简化:
1. 接收
JWT
2. 拆分JWT为EncodedHeader,EncodedPayload,EncodedSignature
3.Header = Base64UrlDecode(EncodedHeader)
4.Payload = Base64UrlDecode(EncodedPayload)
5.ExpectedSignature = Algorithm(EncodedHeader + "." + EncodedPayload, Public/SecretKey)
6. IFBase64UrlEncode(ExpectedSignature)!=EncodedSignatureTHENJWT无效
7. IFCurrentTime > Payload.expTHENJWT过期
8. 验证其他声明(iss,aud, 等)
9. ELSEJWT有效,可信任Payload内容
如何携带JWT进行请求?
客户端在获取到JWT后,通常会将其存储在本地(如浏览器的LocalStorage、SessionStorage或HttpOnly Cookie)。在向受保护的API发起请求时,客户端会将JWT添加到HTTP请求的Authorization头部中,格式为Bearer 。
HTTP请求示例:
GET /api/profile Host: api.example.com Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ilx1NTU2Nlx1NWMzMyIsImFkbWluIjp0cnVlLCJpYXQiOjE1MTYyMzkwMjIsImV4cCI6MTUxNjI0MjYyMn0.yF8Qc7v4J5_d9S8p5kK3kF4B2K0m2Q7t6L7F9b0jK0o
服务器端接收到此请求后,会提取JWT并进行上述的验证过程。
如何处理JWT的刷新与失效?
为了兼顾安全性和用户体验,JWT通常采用“短生命周期访问令牌 + 长生命周期刷新令牌”的策略。
- 访问令牌过期: 当客户端使用过期访问令牌请求资源时,服务器会拒绝请求并返回错误(如HTTP 401 Unauthorized)。
- 使用刷新令牌: 客户端捕获到401错误后,可以使用存储的刷新令牌向认证服务器发起一个特殊的“刷新令牌”请求。
- 生成新令牌: 认证服务器验证刷新令牌的有效性(包括是否过期、是否被撤销等)。如果有效,则签发一个新的访问令牌(和可选的新的刷新令牌)给客户端。
- 更新令牌: 客户端用新的访问令牌替换旧的,并重新发起之前的资源请求。
注意: 刷新令牌通常只在授权服务器存储和验证,且其本身也需要有过期时间,并在特定安全事件发生时(如用户密码修改、强制下线)可被撤销。
【怎么】— 如何确保JWT的安全性?常见风险与防范策略
尽管JWT带来了许多便利,但如果使用不当,也存在安全风险。以下是确保JWT安全性的关键策略:
-
始终使用HTTPS/SSL/TLS:
JWT内容是编码而非加密的,这意味着载荷信息是明文可见的。因此,在客户端和服务器之间传输JWT时,必须始终使用HTTPS协议。这可以防止中间人攻击(Man-in-the-Middle, MITM)窃取JWT,从而导致敏感信息泄露或令牌被盗用。
-
保护好你的密钥(Secret Key / Private Key):
用于签名的密钥是JWT安全的核心。如果攻击者获取了密钥,他们就可以伪造有效的JWT。因此,密钥必须:
- 足够长且复杂: 难以被猜测或暴力破解。
- 安全存储: 绝不能硬编码在代码中,应从环境变量、密钥管理服务(KMS)或安全配置文件中加载。
- 定期轮换: 即使密钥泄露,也能限制其影响范围。
-
设置合适的生命周期(exp):
如前所述,访问令牌应具有较短的生命周期(例如15分钟到1小时)。这大大降低了令牌被盗用后长时间滥用的风险。
结合刷新令牌机制,提供更好的用户体验,同时保持访问令牌的短生命周期。
-
谨慎选择存储位置:
客户端存储JWT的选择会影响安全性:
-
HTTP-only Cookies: 最推荐用于存储访问令牌(特别是在浏览器环境中),因为JavaScript无法直接访问它们,可以有效防御XSS(Cross-Site Scripting)攻击。同时,配置
Secure属性以确保只在HTTPS下发送,并配置SameSite属性(如Lax或Strict)防御CSRF(Cross-Site Request Forgery)攻击。 - LocalStorage / SessionStorage: 方便JavaScript访问,但易受XSS攻击。如果页面存在XSS漏洞,攻击者可以直接读取并窃取存储在LocalStorage中的JWT。因此,如果选择这种方式,必须确保前端应用没有任何XSS漏洞。
-
HTTP-only Cookies: 最推荐用于存储访问令牌(特别是在浏览器环境中),因为JavaScript无法直接访问它们,可以有效防御XSS(Cross-Site Scripting)攻击。同时,配置
-
避免在JWT中存储敏感信息:
由于JWT的载荷是Base64Url编码的,可以被任何人解码读取。因此,绝不能在载荷中放置任何敏感信息,如用户密码、信用卡号、私钥等。JWT只应包含非敏感的、验证授权所需的用户ID、角色等信息。
-
严格验证所有声明:
在服务器端验证JWT时,除了签名,还必须严格验证所有重要的声明,特别是
exp(过期时间)、nbf(未生效时间)、iss(签发者)和aud(受众)。未经验证的JWT是不安全的。 -
防范重放攻击(Replay Attacks):
JWT本身是无状态的,一旦签发并有效,就可以被重复使用直到过期。如果攻击者截获了一个有效的JWT,他们可以在其过期之前多次使用它。防范重放攻击的一些策略包括:
- 短生命周期: 降低攻击窗口。
- 唯一标识 (
jti): 在JWT的载荷中加入一个唯一的jti(JWT ID)声明,并在服务器端维护一个已用过的jti黑名单,但这会重新引入服务器端状态,违背JWT的无状态初衷,因此通常只在特定高安全要求场景下使用。 - 一次性令牌 (Nonce): 结合请求的随机数来验证请求的唯一性,但实现复杂。
-
实施令牌撤销机制(针对特定情况):
虽然JWT提倡无状态,但在某些紧急情况下,如用户注销、密码修改、账户被盗或管理员强制下线,可能需要立即撤销某个JWT的有效性。此时,可以在服务器端维护一个“黑名单”或“吊销列表”来存储被撤销的JWT的
jti或整个JWT。每次验证JWT时,都需要查询这个列表,这又会引入服务器端状态,是一个权衡的过程。 -
强制刷新令牌的使用限制:
如果使用刷新令牌,确保它们也具有过期时间,并且只能用于获取新的访问令牌,而不是直接访问受保护的资源。刷新令牌也应存储在更安全的HttpOnly Cookie中,并且只在认证服务器中进行验证和使用。
-
处理“None”算法漏洞:
早期的某些JWT库可能允许在头部中指定
"alg": "none",这意味着JWT没有签名。如果服务器端未严格检查并拒绝none算法的JWT,攻击者可以伪造一个不带签名的JWT,并声称其有效。因此,务必确保你的JWT库或验证逻辑会拒绝使用none算法的JWT,或只允许明确支持的算法。
总结: JWT的安全性依赖于加密传输、密钥管理、合理生命周期、安全存储和严格验证等多方面的综合策略。它不是一个包治百病的银弹,而是需要精心设计和实现安全流程的认证机制。