理解 JWT 令牌:完整指南
· 12分钟阅读
目录
什么是 JSON Web 令牌 (JWT)?
JSON Web 令牌 (JWT) 已成为现代 Web 应用程序中各方之间安全传输信息的事实标准。由 RFC 7519 定义,JWT 提供了一种紧凑、URL 安全的方法来表示要在两方之间传输的声明。
与服务器维护状态的传统基于会话的身份验证不同,JWT 是自包含的。这意味着令牌本身携带验证用户身份和权限所需的所有信息。这种无状态特性使 JWT 在分布式系统、微服务架构和移动应用程序中特别有价值。
JWT 在现代应用程序中有两个主要用途:
- 授权: 用户登录后,每个后续请求都包含 JWT,允许用户访问该令牌允许的路由、服务和资源。单点登录 (SSO) 实现由于其小开销和跨域能力而严重依赖 JWT。
- 信息交换: JWT 提供了一种在各方之间传输信息的安全方式。因为 JWT 可以使用公钥/私钥对进行签名,您可以验证发送者是否是他们声称的身份,以及内容是否未被篡改。
JWT 的美妙之处在于其简单性和多功能性。它们可以在不同的编程语言和平台之间无缝工作,使其成为异构环境的理想选择,其中您的前端可能是 React,后端是 Node.js,移动应用程序是 Swift 或 Kotlin。
专业提示: 虽然 JWT 非常有用,但它们并不是所有身份验证场景的万能解决方案。了解何时使用 JWT 而不是传统会话对于构建安全、可扩展的应用程序至关重要。
JWT 详细结构和剖析
JWT 由三个不同的部分组成,用点 (.) 分隔,形成如下结构:xxxxx.yyyyy.zzzzz。每个部分都有特定的用途,它们共同创建了一个防篡改的信息包。
头部
头部通常由两部分组成:令牌类型 (JWT) 和正在使用的签名算法,例如 HMAC SHA256 或 RSA。此信息告诉接收方如何验证令牌的签名。
{
"alg": "HS256",
"typ": "JWT"
}
然后头部被 Base64Url 编码以形成 JWT 的第一部分。您会遇到的常见算法包括:
- HS256 (HMAC with SHA-256): 使用共享密钥的对称算法
- RS256 (RSA Signature with SHA-256): 使用公钥/私钥对的非对称算法
- ES256 (ECDSA with SHA-256): 使用椭圆曲线密码学的非对称算法
载荷
载荷包含声明,这些声明是关于实体(通常是用户)和附加元数据的陈述。声明分为三种类型:
注册声明: 这些是预定义的声明,不是强制性的,但建议使用以提供互操作性。它们包括:
iss(issuer): 标识谁颁发了令牌sub(subject): 标识令牌的主题(通常是用户 ID)aud(audience): 标识 JWT 的预期接收者exp(expiration time): JWT 过期后的时间戳nbf(not before): JWT 不得被接受之前的时间戳iat(issued at): JWT 颁发时的时间戳jti(JWT ID): JWT 的唯一标识符
公共声明: 这些是您为应用程序定义的自定义声明。为避免冲突,它们应在 IANA JSON Web Token Registry 中定义或使用抗冲突名称(如命名空间 URI)。
私有声明: 创建的自定义声明,用于在同意使用它们的各方之间共享信息。这些既不是注册声明也不是公共声明。
{
"sub": "1234567890",
"name": "John Doe",
"email": "[email protected]",
"role": "admin",
"iat": 1516239022,
"exp": 1516242622
}
然后载荷被 Base64Url 编码以形成 JWT 的第二部分。
快速提示: 切勿在 JWT 载荷中存储敏感信息,如密码或信用卡号。虽然 JWT 已签名,但默认情况下它们不加密,这意味着任何人都可以解码和读取载荷。
签名
签名是通过获取编码的头部、编码的载荷、密钥(对于 HMAC 算法)或私钥(对于 RSA/ECDSA),并使用头部中指定的算法对它们进行签名来创建的。
例如,如果使用 HMAC SHA256,签名将像这样创建:
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
签名确保令牌未被更改。如果有人更改头部或载荷中的单个字符,签名验证将失败。
完整的 JWT 示例
当您将所有三个部分放在一起时,您会得到一个完整的 JWT,如下所示:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
您可以使用我们的 JWT 解码器工具 解码和检查任何 JWT,以查看其头部、载荷并验证其签名。
在实践中创建和使用 JWT
使用正确的库创建 JWT 非常简单。让我们探讨如何在不同的编程语言和场景中生成和使用 JWT。
在 Node.js 中创建 JWT
jsonwebtoken 库是 Node.js 应用程序最流行的选择:
const jwt = require('jsonwebtoken');
// 创建令牌
const payload = {
sub: user.id,
email: user.email,
role: user.role
};
const secret = process.env.JWT_SECRET;
const options = {
expiresIn: '1h',
issuer: 'myapp.com'
};
const token = jwt.sign(payload, secret, options);
// 验证令牌
try {
const decoded = jwt.verify(token, secret);
console.log(decoded);
} catch (error) {
console.error('无效令牌:', error.message);
}
在 Python 中创建 JWT
Python 开发人员通常使用 PyJWT 库:
import jwt
import datetime
# 创建令牌
payload = {
'sub': user.id,
'email': user.email,
'role': user.role,
'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=1),
'iat': datetime.datetime.utcnow()
}
secret = os.environ.get('JWT_SECRET')
token = jwt.encode(payload, secret, algorithm='HS256')
# 验证令牌
try:
decoded = jwt.decode(token, secret, algorithms=['HS256'])
print(decoded)
except jwt.ExpiredSignatureError:
print('令牌已过期')
except jwt.InvalidTokenError:
print('无效令牌')
在 Java 中创建 JWT
对于 Java 应用程序,Auth0 的 java-jwt 库被广泛使用:
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
// 创建令牌
Algorithm algorithm = Algorithm.HMAC256(secret);
String token = JWT.create()
.withSubject(user.getId())
.withClaim("email", user.getEmail())
.withClaim("role", user.getRole())
.withExpiresAt(new Date(System.currentTimeMillis() + 3600000))
.withIssuer("myapp.com")
.sign(algorithm);
// 验证令牌
try {
DecodedJWT jwt = JWT.require(algorithm)
.withIssuer("myapp.com")
.build()
.verify(token);
} catch (Exception e) {
System.out.println("无效令牌: " + e.getMessage());
}
令牌生成的最佳实践
创建 JWT 时,请遵循以下准则以确保安全性和可靠性:
- 使用强密钥: 您的 JWT 密钥应至少为 256 位(32 个字符)的随机数据。切勿在源代码中硬编码密钥。
- 设置适当的过期时间: 访问令牌应该是短期的(15 分钟到 1 小时),而刷新令牌可以持续更长时间(几天到几周)。
- 仅包含必要的声明: 保持载荷精简以减少令牌大小并最小化信息暴露。
- 使用正确的算法: 对于大多数应用程序,HS256 就足够了。当您需要在不共享密钥的情况下验证令牌时(如在微服务中),请使用 RS256。
专业提示: 考虑使用刷新令牌实现令牌刷新机制。这允许您出于安全考虑保持访问令牌的短期性,同时在不频繁重新身份验证的情况下保持良好的用户体验。
JWT 身份验证工作流程详解
了解 JWT 如何在应用程序中流动对于正确实现它们至关重要。让我们逐步了解典型的身份验证工作流程。
初始身份验证
- 用户提交凭据: 用户将其用户名和密码发送到身份验证端点(通常是
POST /api/auth/login)。 - 服务器验证凭据: 服务器根据数据库检查凭据,验证密码哈希是否匹配。
- 服务器生成 JWT: 验证成功后,服务器创建一个包含用户身份和相关声明的 JWT。
- 服务器返回令牌: JWT 被发送回客户端,通常在响应正文中。一些实现还将其设置为 HTTP-only cookie。
后续请求
- 客户端包含令牌: 对于每个后续请求,客户端在 Authorization 头中包含 JWT:
Authorization: Bearer <token> - 服务器验证令牌: 服务器提取令牌,验证其签名并检查其过期时间。
- 服务器处理请求: 如果令牌有效,服务器从载荷中提取用户信息并相应地处理请求。
- 服务器返回响应: 请求的数据或操作结果返回给客户端。
令牌刷新流程
当访问令牌过期时,刷新流程启动:
- 客户端检测过期: 客户端收到 401 未授权响应或检查令牌的
exp声明。 - 客户端发送刷新令牌: 客户端将刷新令牌发送到专用刷新端点(
POST /api/auth/refresh)。 - 服务器验证刷新令牌: 服务器验证刷新令牌并检查它是否已被撤销。
- 服务器颁发新的访问令牌: 生成新的访问令牌并返回给客户端。
- 客户端重试原始请求: 客户端使用新的访问令牌重试失败的请求。
注销流程
使用 JWT 注销需要特别考虑,因为令牌是无状态的:
- 客户端启动注销: 用户点击注销,触发对
POST /api/auth/logout的请求。 - 服务器使刷新令牌无效: 服务器将刷新令牌添加到撤销列表或从数据库中删除它。
- 客户端丢弃令牌: 客户端从存储中删除访问令牌和刷新令牌。
- 客户端重定向: 用户被重定向到登录页面或公共区域。
| 令牌类型 | 典型生命周期 | 存储位置 | 用途 |
|---|---|---|---|
| 访问令牌 | 15 分钟 - 1 小时 | 内存或 sessionStorage | 授权 API 请求 |
| 刷新令牌 | 7 天 - 30 天 | HTTP-only cookie 或安全存储 | 获取新的访问令牌 |
| ID 令牌 | 与访问令牌相同 | 内存 | 用户身份信息 (OpenID Connect) |
快速提示: 在客户端应用程序中实现自动令牌刷新以无缝处理令牌过期。这可以防止用户意外注销