现代Web应用离不开用户认证。无论是登录社交账号、调用API接口,还是使用移动应用,背后都有认证机制在工作。在众多认证方案中,JWT(JSON Web Token)已经成为最流行的选择之一。它简洁、自包含、易于扩展,被广泛应用于单点登录、API认证、微服务架构等场景。
什么是JWT
JWT的全称是JSON Web Token,读音与英文单词"jot"相同。它是一种开放标准(RFC 7519),定义了一种紧凑且URL安全的方式,用于在各方之间以JSON对象的形式安全地传输信息。
JWT的核心思想很简单:服务器认证用户后,生成一个包含用户身份信息的令牌(Token)返回给客户端。客户端在后续请求中携带这个令牌,服务器通过验证令牌的签名来确认用户身份,而不需要查询数据库或Session存储。
2015年5月,IETF正式发布RFC 7519,将JWT标准化。如今,JWT已成为OAuth 2.0和OpenID Connect协议的重要组成部分,几乎所有现代身份认证系统都在使用它。
JWT的结构:三个部分组成一个完整的令牌
一个实际的JWT看起来像这样一长串字符:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IuS4iua1t-W4puS8miIsImlhdCI6MTUxNjIzOTAyMn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
这串字符由三部分组成,用点号(.)分隔:Header(头部)、Payload(负载) 和 Signature(签名)。

图片来源: jwt.io
Header:描述令牌的元数据
Header是一个JSON对象,描述令牌的类型和签名算法。例如:
{
"alg": "HS256",
"typ": "JWT"
}
alg(algorithm):指定签名算法,HS256表示使用HMAC SHA-256算法typ(type):指定令牌类型,通常为JWT
这个JSON对象会使用Base64URL算法编码,形成JWT的第一部分。Base64URL是Base64的一个变体,专门设计用于URL安全传输——它将+替换为-,将/替换为_,并去掉末尾的=填充符号。
Payload:携带实际的数据
Payload是JWT的核心,包含需要传递的数据。这些数据以"声明"(Claim)的形式存在,每个声明都是一个键值对。
RFC 7519定义了七个注册声明(Registered Claims),它们有特定的含义:
| 声明 | 全称 | 含义 |
|---|---|---|
iss |
Issuer | 令牌的签发者 |
sub |
Subject | 令牌的主题(通常是用户ID) |
aud |
Audience | 令牌的接收者 |
exp |
Expiration Time | 过期时间 |
nbf |
Not Before | 生效时间 |
iat |
Issued At | 签发时间 |
jti |
JWT ID | 令牌的唯一标识 |
除了注册声明,你可以添加自定义声明。例如:
{
"sub": "user-12345",
"name": "张三",
"role": "admin",
"iat": 1516239022,
"exp": 1516242622
}
重要提示:Payload只是Base64URL编码,不是加密!任何人都可以解码并读取其中的内容。因此,永远不要在JWT中存储敏感信息,如密码、信用卡号或身份证号。
Signature:保证令牌不被篡改
Signature是JWT的安全核心,用于验证令牌的完整性和真实性。签名过程如下:
-
将编码后的Header和Payload用点号连接:
base64UrlEncode(header) + "." + base64UrlEncode(payload) -
使用Header中指定的算法和密钥对连接后的字符串进行签名。以HS256为例:
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret ) -
将签名结果进行Base64URL编码,形成JWT的第三部分。
服务器收到JWT后,会使用相同的密钥和算法重新计算签名,并与令牌中的签名进行比对。如果一致,说明令牌没有被篡改;如果不一致,则拒绝该令牌。
JWT的认证流程
理解JWT的结构后,来看它在实际认证中是如何工作的。
sequenceDiagram
participant User as 用户
participant Client as 客户端
participant Server as 服务器
User->>Client: 输入用户名密码
Client->>Server: POST /login {username, password}
Server->>Server: 验证凭据
Server->>Server: 生成JWT
Server-->>Client: 返回JWT
Client->>Client: 存储JWT
Note over User,Server: 后续请求
Client->>Server: GET /api/user<br/>Authorization: Bearer <token>
Server->>Server: 验证JWT签名
Server->>Server: 解析Payload获取用户信息
Server-->>Client: 返回用户数据
完整的认证流程包含以下步骤:
-
用户登录:用户向服务器发送用户名和密码。
-
服务器验证:服务器验证凭据是否正确。
-
签发令牌:验证通过后,服务器生成JWT并返回给客户端。
-
客户端存储:客户端将JWT存储起来(通常存储在localStorage或Cookie中)。
-
携带令牌请求:客户端在后续请求的Authorization头部中携带JWT:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... -
服务器验证:服务器验证JWT的签名、过期时间等信息。
-
返回响应:验证通过后,服务器处理请求并返回响应。
JWT与Session:两种认证模型的对比
传统的Session认证和JWT认证是两种不同的模型,各有优缺点。
Session认证的工作方式
传统Session认证流程:
- 用户登录后,服务器创建一个Session,存储用户信息
- 服务器返回一个Session ID给客户端(通常存储在Cookie中)
- 后续请求时,客户端自动发送Cookie中的Session ID
- 服务器根据Session ID查找对应的Session数据
JWT认证的特点
JWT采用无状态设计,服务器不需要存储任何会话信息:
| 特性 | Session | JWT |
|---|---|---|
| 存储位置 | 服务器端 | 客户端 |
| 扩展性 | 需要Session共享 | 天然支持分布式 |
| 跨域支持 | 需要额外配置 | 原生支持 |
| 注销能力 | 立即生效 | 需等过期或额外逻辑 |
| 信息容量 | 仅Session ID | 可携带自定义数据 |
什么时候选择JWT
JWT特别适合以下场景:
- 分布式系统和微服务架构:多个服务之间无需共享Session存储
- 移动应用和单页应用(SPA):前后端分离架构
- 单点登录(SSO):一次登录,多个系统共享身份
- API服务:无状态的RESTful API
但对于传统的Web应用,尤其是需要精细控制会话生命周期的场景,Session仍然是一个可靠的选择。
签名算法的选择
JWT支持多种签名算法,最常见的有三种:
HS256:对称加密
HS256(HMAC SHA-256)使用同一个密钥进行签名和验证。它的特点是:
- 计算速度快
- 实现简单
- 密钥必须妥善保管,任何拥有密钥的人都可以签发令牌
适合单服务内部使用,但不适合需要多方验证的场景。
RS256:非对称加密
RS256(RSA SHA-256)使用公私钥对:
- 私钥用于签名,只有授权服务器持有
- 公钥用于验证,可以分发给任何需要验证JWT的服务
- 计算较慢,但安全性更高
适合微服务架构和第三方集成的场景。
ES256:椭圆曲线签名
ES256(ECDSA P-256)是一种更现代的非对称算法:
- 比RSA更短的密钥,相同的强度
- 签名和验证速度更快
- 实现复杂度稍高
对于新项目,如果性能和密钥长度是考虑因素,ES256是一个不错的选择。
根据Curity的安全建议,在性能和安全性之间,ES256和EdDSA是当前最推荐的算法选择。
安全最佳实践
使用JWT时,安全性是首要考虑因素。以下是关键的安全实践:
1. 验证所有必要的声明
服务器在验证JWT时,必须检查:
- 签名:确保令牌未被篡改
- 过期时间(exp):拒绝已过期的令牌
- 签发者(iss):确认令牌来自可信来源
- 接收者(aud):确认令牌是颁发给你的服务
// 示例验证逻辑
function verifyJWT(token, secret) {
const decoded = jwt.verify(token, secret, {
issuer: 'https://api.example.com',
audience: 'my-app'
});
if (decoded.exp < Date.now() / 1000) {
throw new Error('Token expired');
}
return decoded;
}
2. 设置合理的过期时间
JWT一旦签发,在过期之前都无法撤销。因此,应该设置较短的过期时间——通常几分钟到几小时。对于需要长期保持登录的场景,可以使用Refresh Token机制。
3. 安全存储令牌
客户端存储JWT有两种主要方式:
localStorage:容易受到XSS(跨站脚本)攻击,恶意JavaScript可以读取其中的令牌。
HttpOnly Cookie:设置了HttpOnly标志的Cookie无法被JavaScript读取,可以防止XSS攻击。但需要注意防范CSRF(跨站请求伪造)攻击,通常配合SameSite属性使用。
RFC 8725建议,对于Web应用,优先使用HttpOnly Cookie存储JWT。
4. 不要存储敏感信息
JWT的Payload是Base64URL编码,任何人都可以解码。永远不要在其中存储:
- 密码或密码哈希
- 个人敏感信息(身份证号、银行卡号)
- 内部系统信息(数据库连接字符串、API密钥)
5. 使用HTTPS传输
JWT应该始终通过HTTPS传输,防止中间人攻击窃取令牌。
Refresh Token机制
为了解决JWT过期时间短导致用户频繁登录的问题,业界引入了Refresh Token机制。
工作原理:
-
用户登录时,服务器返回两个令牌:
- Access Token:有效期短(如15分钟),用于访问资源
- Refresh Token:有效期长(如7天),用于获取新的Access Token
-
当Access Token过期时,客户端使用Refresh Token请求新的Access Token。
-
服务器验证Refresh Token后,签发新的Access Token(可选:同时签发新的Refresh Token)。
-
如果Refresh Token也过期或被撤销,用户需要重新登录。
这种设计既保证了安全性(Access Token有效期短),又提供了良好的用户体验。
sequenceDiagram
participant Client as 客户端
participant Auth as 认证服务器
participant API as API服务器
Client->>Auth: 登录请求
Auth-->>Client: Access Token + Refresh Token
Client->>API: 请求资源 (Access Token)
API-->>Client: 返回数据
Note over Client: Access Token过期
Client->>Auth: 刷新令牌 (Refresh Token)
Auth-->>Client: 新的 Access Token
Client->>API: 请求资源 (新Access Token)
API-->>Client: 返回数据
常见问题解答
JWT可以被撤销吗?
标准JWT一旦签发,在过期前无法撤销。如果需要撤销功能,需要在服务端维护一个黑名单,或使用较短的过期时间配合Refresh Token。
如何处理令牌泄露?
如果怀疑JWT泄露,应该立即让用户重新登录,并签发新的令牌。在服务端可以维护一个令牌版本号,强制使旧令牌失效。
JWT适合作为Session的替代品吗?
这取决于具体需求。JWT的设计初衷是无状态认证,不是替代Session。对于需要精细会话管理、实时权限更新的场景,Session仍然更合适。
总结
JWT是一种简洁、自包含的令牌格式,通过签名保证数据的完整性。它的三段式结构——Header、Payload、Signature——各司其职,共同构成一个安全的认证凭证。
理解JWT的关键在于:
- Payload不是加密,不要存储敏感信息
- 签名算法的选择影响性能和安全性
- 无状态设计带来了扩展性,但也限制了撤销能力
- 安全实践(HTTPS、短过期时间、HttpOnly Cookie)缺一不可
在实际项目中,应该根据具体需求选择JWT或Session,而不是盲目跟风。对于分布式系统、移动应用和API认证,JWT是一个优秀的选择;对于传统Web应用,Session可能更简单可靠。