FlowingDev

JWT 详解:自带护照的令牌

深入了解 JSON Web Token (JWT)——一种紧凑、自包含的标准,用于在各方之间安全地传输 JSON 对象信息。

试用工具: JWT 查看器

一句话概括

JSON Web Token (JWT) 是一种紧凑且 URL 安全的方式,用于在双方之间传递声明(claims),其可被验证和信任,通常用于身份验证和授权。

它解决了什么问题

在很久很久以前——比方说,21 世纪初那会儿——如果你登录一个网站,服务器会为你创建一个“会话(session)”。这就像服务器上的一个小文件,记录着“用户 123 已登录,购物车里放了一只橡皮鸡”。然后服务器会给你的浏览器一个带会话 ID 的小 cookie,就像一张衣帽间的寄存牌。在之后的每一次请求中,你的浏览器都会出示这张“寄存牌”,服务器查找对应的文件,然后想起你是谁。

对于单个、庞大的单体服务器来说,这套机制运行良好。但后来,Web 世界迎来了大爆炸。我们有了微服务、单页应用(SPA)和移动应用,它们都和同一个后端打交道。现在,你的登录请求可能发给了服务器 A,但下一个获取你个人资料的请求可能就跑到了服务器 B。服务器 B 怎么知道服务器 A 上的会话文件呢?

你可以强制用户总是和同一个服务器通信(也就是“粘性会话”),但这会造成瓶颈。你也可以创建一个所有服务器共享的中心化会话数据库(比如 Redis),但这又增加了一个需要管理的基础设施,也多了一个潜在的故障点。

核心问题在于状态性(statefulness)。服务器必须“记住”你。

JWT(读作 'jot')则彻底颠覆了这种思路。要是用户能自己带着身份证明(就像一本护照),那会怎么样?令牌(Token)本身就包含了服务器需要的所有信息:用户是谁、他们被允许做什么、以及他们的访问权限何时过期。这样,服务器在两次请求之间就无需记住任何事情。这就是无状态(stateless)认证,也是构建可扩展、分布式系统的关键。服务器只需要检查这本“护照”(JWT)是否有效、有没有被伪造。

工作原理揭秘

JWT 不是一坨看不懂的乱码。它是一个结构非常明确的字符串,由三个部分组成,并用点(.)分隔。

xxxxx.yyyyy.zzzzz

我们来逐一分解每个部分。

Header (头部):类型标签

第一部分是头部(Header)。它是一个简单的 JSON 对象,包含了关于令牌本身的元数据,主要是所使用的签名算法和令牌类型。

{
  "alg": "HS256",
  "typ": "JWT"
}
  • alg: 签名算法。HS256 表示这个令牌是用 HMAC-SHA256 签名的,这是一种对称算法(稍后详述)。其他常见选项包括 RS256(使用 RSA 公私钥对)。
  • typ: 令牌类型。对 JWT 来说,这里就是 "JWT"。

这个 JSON 对象随后会经过 Base64Url 编码,生成令牌的第一部分。Base64 是一种编码方案,不是加密。它只是把二进制数据转换成可以在网络上传输的文本字符串。把它想象成在明信片背面写上“这是一张明信片”——任何截获它的人都能读懂。

Payload (载荷):声明信息部

第二部分是载荷(Payload)。这部分是干货。它是另一个 JSON 对象,包含了“声明(claims)”,也就是关于用户(即“主体”)的陈述以及其他有用数据。

{
  "sub": "10987-23456-98765",
  "name": "Grace Hopper",
  "admin": true,
  "iat": 1516239022,
  "exp": 1516242622
}

声明分为三种:

  • 注册声明 (Registered Claims): 这是一组预定义的、推荐使用的声明,以提供互操作性。它们不是强制性的,但超级有用。
声明 名称 描述
iss Issuer (签发者) 令牌是谁签发的 (例如, https://api.mycoolsite.com)。
sub Subject (主题) 令牌是关于哪个用户或实体的 (例如, 用户 ID)。
aud Audience (受众) 令牌是发给谁的 (例如, https://api.mycoolsite.com)。
exp Expiration Time (过期时间) 令牌何时过期。一个数字类型的 Unix 时间戳 (从 epoch 开始的秒数)。
iat Issued At (签发时间) 令牌是何时签发的。同样是 Unix 时间戳。
  • 公共声明 (Public Claims): 这些是你创建的自定义声明,但为了避免命名冲突,它们应该在 IANA JSON Web Token Claims registry 中定义,或者是一个包含防冲突命名空间的 URI。
  • 私有声明 (Private Claims): 这些是最常见的自定义声明,用于在同意使用它们的各方之间共享信息(就像我们例子中的 admin: true)。这里就是你放置应用特定数据的地方。

和头部一样,整个载荷 JSON 对象也会经过 Base64Url 编码,形成 JWT 的第二部分。再次强调,这不是加密。永远不要在载荷里放密码之类的敏感信息。

Signature (签名):防伪封条

这部分提供了安全性。签名用于验证 JWT 的发送者确实是它所声称的那个人,并确保消息在传输过程中没有被篡改。

签名的创建过程是:将编码后的头部、编码后的载荷和一个密钥(secret)一起,通过头部指定的算法进行处理。对于我们的 HS256 例子,过程如下:

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  your-256-bit-secret
)

关键就在于:这个过程用到了一个只有服务器才知道的secret(密钥)。当服务器收到一个 JWT 时,它会用收到的头部和载荷,以及它自己保存的密钥,重新执行完全相同的计算。如果它生成的签名与令牌上的签名匹配,服务器就能确定两件事:

  1. 真实性 (Authenticity): 这个令牌是由知道密钥的人(也就是服务器自己)创建的。
  2. 完整性 (Integrity): 头部和载荷没有被篡改过。如果攻击者把载荷里的 "admin": false 改成了 "admin": true,签名就会对不上了。

这个签名,就是我们这本“护照”上的防伪全息封条。

真实世界的应用

微服务迷宫

一家快速增长的电商公司 "ScaleFast" 决定将其庞大的单体后端拆分成一系列微服务:一个管用户,一个管订单,一个管库存,等等。旧系统使用服务器端会话。但在新世界里,OrderService(订单服务)怎么知道一个请求真的来自一个已登录的用户,而不需要每次都去调用 UserService(用户服务)呢?那样做太慢了,也违背了解耦的初衷。

解决方案就是 JWT。当用户登录时,新的 AuthService(认证服务)会签发一个包含 userId 和用户 roles(角色)的 JWT。用户的浏览器随后在每次请求其他微服务时,都在 Authorization 请求头里带上这个 JWT。OrderService 和 InventoryService 不需要和 AuthService 通信;它们只需要知道共享的密钥。它们可以独立地验证 JWT 的签名,信任里面的 userId,然后处理请求。

经验: JWT 是微服务认证的通用语,它让服务变得无状态且可独立验证。

单页应用传奇

一位名叫 Alex 的开发者正在构建一个酷炫的 React 仪表盘。前端是一个托管在静态服务器上的单页应用(SPA),它与一个独立的后端 API 通信。Alex 最初在跟传统的基于 cookie 的认证方式作斗争,结果因为前端和后端在不同域上,陷入了跨域资源共享(CORS)问题的噩梦。

团队转而使用 JWT。现在,用户用用户名和密码登录后,API 会返回一个 JWT。Alex 的 React 应用将这个令牌存储在内存中,并附加到每个 API 调用上:Authorization: Bearer <the-jwt>。API 后端是无状态的;它只需检查每个传入请求的 bearer token。再也没有 CORS 和 cookie 带来的头痛了。

经验: JWT 提供了一种干净、可移植的凭证,非常适合将现代前端应用与后端 API 解耦。

常见的误区和陷阱

  • 在载荷中存放敏感数据。 快住手!载荷是 Base64Url 编码的,可以被轻易逆向解码。它没有被加密。任何拿到令牌的人都可以读取载荷。把它当作一张明信片,而不是一封密封的信。
  • 忘记验证签名。 如果边检员不检查护照上的防伪特征,那这些特征还有什么意义?仅仅解码载荷并信任其内容而不验证签名,是一个灾难性的安全漏洞。攻击者可以随心所欲地伪造任何他们想要的载荷。
  • 盲目信任 alg 头部。 过去一个著名的漏洞是,攻击者创建一个令牌,并将头部改为 {"alg": "none"}。一些配置不当的库看到 "none" 后,就会通过……什么都不做……来“验证”签名,从而接受了这个伪造的令牌。你的服务器应该始终强制使用一个特定的、预期的算法(例如 HS256)。
  • 泄露你的对称密钥。 对于像 HS256 这样的 HMAC 算法,密钥就是王国的钥匙。如果它泄露了,任何人都可以为任何用户伪造具有任何权限的令牌。像保护密码一样保护它。
  • 不设置过期时间(exp 声明)。 一个永久有效的令牌是一个巨大的隐患。如果它被泄露,攻击者就可以无限期地使用它。始终设置一个合理的短过期时间,并使用刷新令牌(refresh token)机制来实现更长久的会话。

为什么你应该关注它

只要你在分布式环境中处理身份验证或授权,就应该想到 JWT。

  • 你正在为单页应用(SPA)或移动客户端构建 API。
  • 你正在设计一个微服务架构,其中服务之间需要相互信任请求。
  • 你需要无状态的身份验证,以便在没有共享会话存储的情况下进行水平扩展。
  • 你正在实现一次性的授权流程,比如密码重置链接或电子邮件验证,这种场景下,一个自包含、可过期的令牌是完美的选择。

它是当今安全表示声明的现代标准,理解它的优点——以及缺点——对今天的开发者来说是必备技能。

深入了解

理论搞定,动手试试吧——100% 在你的浏览器中运行。

试用工具: JWT 查看器