在现代网络应用和分布式系统中,认证 (Authentication) 和授权 (Authorization) 是安全性的基石。JSON Web Token(简称 JWT,读作 /dʒɒt/)作为一种开放标准 (RFC 7519),为安全地传输信息提供了一种紧凑且自包含的解决方案。 本教程将深入探讨 JWT 的方方面面,从核心概念、结构、工作流程到安全实践,为你提供一份内容详实、全面易懂的指南。
想象一下传统的基于会话 (Session) 的认证方式。用户登录后,服务器会创建一个会话并将其存储起来(可能在内存或数据库中),然后将一个会话ID(Session ID)通过 Cookie 发送给客户端。之后客户端的每次请求都需要携带这个 Session ID,服务器通过它来查找对应的会话信息,以验证用户身份和权限。
这种方式在单体应用中运行良好,但在分布式或微服务架构中却面临挑战:
扩展性问题:如果有多台服务器,需要共享会 Sessi on 数据,这增加了系统的复杂性。
状态维护:服务器需要维护大量的会话状态,增加了服务器的负担。
跨域限制:基于 Cookie 的方式在跨域场景下会遇到诸多限制。
JWT 的出现正是为了解决这些问题。它是一种无状态的 (stateless)、自包含的 (self-contained) 认证机制。
无状态:服务器端无需保存任何关于用户会话的信息。所有必要的信息都包含在 Token 本身。
自包含:Token 内部包含了验证用户身份和权限所需的所有信息,减少了对数据库的查询次数。
简单来说,JWT 就像一张经过数字签名的身份证。当用户登录成功后,服务器会签发一张包含用户信息的“数字身份证”(即 JWT)给用户。之后,用户每次访问受保护的资源时,只需出示这张“身份证”,服务器检查一下签名是否有效、信息是否被篡改,就能确认用户的身份和权限,而无需再去数据库里翻阅档案。
一个完整的 JWT 由三部分组成,并通过点号 (.) 进行分隔。这三部分都是经过 Base64Url 编码的字符串。
xxxxx.yyyyy.zzzzz
这三部分分别是:
头部通常由两部分组成:令牌的类型(typ),即 "JWT",以及所使用的签名算法(alg),例如 HMAC SHA256 或 RSA。
一个典型的头部 выглядит 如下:
{
"alg": "HS256",
"typ": "JWT"
}
这个 JSON 对象会经过 Base64Url 编码,形成 JWT 的第一部分。
载荷部分包含了所谓的“声明 (Claims)”。声明是关于实体(通常是用户)和附加元数据的陈述。 声明分为三种类型:
注册声明 (Registered Claims):这是一组预定义的声明,虽然不是强制性的,但推荐使用,以提供一组有用的、可互操作的声明。常见的注册声明包括:
iss (Issuer):签发者
sub (Subject):主题,通常是用户的唯一标识符
aud (Audience):接收者
exp (Expiration Time):过期时间戳,所有实现都必须验证此声明
nbf (Not Before):在此时间戳之前,该 JWT 不可用
iat (Issued At):签发时间戳
jti (JWT ID):JWT 的唯一标识符
公共声明 (Public Claims):这些声明可以由使用 JWT 的人随意定义,但为了避免冲突,应在 IANA JSON Web Token Registry 中定义它们,或将其定义为包含抗冲突命名空间的 URI。
{
"sub": "1234567890",
"name": "John Doe",
"admin": true,
"iat": 1516239022
}
与头部一样,载荷也会经过 Base64Url 编码,形成 JWT 的第二部分。
重要提示: 载荷部分只是进行了 Base64Url 编码,并没有加密。这意味着任何人都可以解码并读取其中的内容。因此,切勿在载荷中存放任何敏感信息,如用户密码。
签名部分是 JWT 安全性的核心。要创建签名,需要将编码后的头部、编码后的载荷、一个密钥 (secret) 以及头部中指定的签名算法进行计算。
例如,如果使用 HMAC SHA256 算法,签名的创建过程如下:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret
)
这个签名用于验证消息在传递过程中没有被篡改,并且,如果令牌是使用私钥签名的,它还可以验证 JWT 的发送者确实是它所声称的身份。 只有持有密钥的服务端才能生成和验证这个签名。
现在我们来梳理一下 JWT 在认证和授权中的完整工作流程:
HttpOnly Cookie 或浏览器的安全存储中(如 localStorage 或 sessionStorage,但需注意安全风险)。Authorization 头部中,使用 Bearer 模式:
Authorization: Bearer <token>
`
6. **服务器验证**:当服务器收到请求后,会检查 Authorization 头部中的 JWT。
7. **签名验证**:服务器使用自己的密钥来验证 JWT 的签名。
* 如果签名有效,服务器就可以信任这个 JWT 中包含的载荷信息。
* 如果签名无效,说明 Token 可能被篡改或不是由该服务器签发的,请求将被拒绝。
8. **声明验证**:服务器还会验证载荷中的注册声明,例如检查 exp 声明以确保令牌没有过期。
9. **授权与响应**:一旦令牌验证通过,服务器就可以根据令牌中的信息(如用户ID、角色等)来处理请求,并返回相应的资源。
### JWT 的主要用例
JWT 的应用场景非常广泛,主要包括:
* **认证与授权 (Authorization)**:这是最常见的用例。用户一旦登录,后续的每个请求都包含 JWT,允许用户访问该令牌授权的路由、服务和资源。 由于其开销小且易于跨域使用,它在单点登录 (Single Sign-On, SSO) 场景中也得到了广泛应用。
* **信息交换 (Information Exchange)**:JWT 是在各方之间安全地传输信息的好方法。因为 JWT 可以被签名(例如,使用公钥/私钥对),所以你可以确定发件人就是他们所说的那个人。 此外,由于签名是使用头部和载荷计算的,因此你还可以验证内容是否未被篡改。
### 安全性考量与最佳实践
尽管 JWT 功能强大,但不正确的使用会带来严重的安全风险。以下是一些必须遵循的最佳实践:
1. **选择强大的签名算法**:
* **避免使用 none 算法**:一些库曾存在漏洞,允许攻击者将算法修改为 none,从而绕过签名验证。服务器端必须强制检查并拒绝 alg 为 none 的令牌。
* **使用强算法**:优先选择如 RS256 (RSA) 或 ES256 (ECDSA) 这样的非对称算法,而不是 HS256 (HMAC) 等对称算法,尤其是在分布式系统中。
2. **密钥必须保密 (Keep Your Secret Secret!)**:
* 签名密钥是 JWT 安全的命脉。必须妥善保管,绝不能泄露到客户端。
* 在生产环境中,应将密钥存储在环境变量或安全的配置管理服务中。
3. **始终验证签名**:
* 接收到 JWT 后,必须做的第一件事就是验证其签名。 任何签名验证失败的令牌都应立即丢弃。
4. **设置合理的过期时间 (exp)**:
* 为所有令牌设置一个较短的过期时间,例如几分钟或几小时。 这可以有效降低令牌泄露后被滥用的风险。
* 可以配合刷新令牌 (Refresh Token) 机制来提供更长的用户会话,同时保持访问令牌 (Access Token) 的短期有效性。
5. **不要在载荷中存放敏感信息**:
* 再次强调,JWT 的载荷是公开可读的。绝对不要在其中存放密码、信用卡号等任何敏感数据。
6. **安全地存储 JWT**:
* 避免将 JWT 存储在 localStorage 中,因为它容易受到跨站脚本 (XSS) 攻击。
* 推荐的做法是将其存储在 HttpOnly 和 Secure 标记的 Cookie 中,这可以防止客户端 JavaScript 访问它,并确保它只通过 HTTPS 传输。
7. **实施令牌撤销机制**:
* JWT 本身是无状态的,这意味着一旦签发,在它过期之前都会有效。 如果需要实现强制用户登出或在密码更改后立即使旧令牌失效的功能,就需要一个令牌撤销机制,例如维护一个黑名单。但这在一定程度上牺牲了 JWT 的无状态优势。
8. **验证 iss 和 aud 声明**:
* 在处理令牌时,务必验证 iss (签发者) 和 aud` (接收者) 声明,确保令牌是由受信任的签发者为你的应用签发的。
JSON Web Token 提供了一种优雅、强大且可扩展的方式来处理现代 Web 应用中的认证和信息交换。 它的无状态和自包含特性使其特别适用于微服务和分布式架构。然而,开发者必须深刻理解其工作原理和潜在的安全风险,并严格遵循安全最佳实践,才能充分利用 JWT 的优势,构建安全可靠的应用程序。