JWT 令牌详解:现代 Web 应用的身份验证
· 12 分钟阅读
目录
JSON Web Token(JWT)已成为现代 Web 应用身份验证的事实标准。它们提供了一种紧凑、URL 安全的方式来表示双方之间的声明,实现了无状态身份验证,无需共享会话存储即可水平扩展。
无论您是在构建 REST API、微服务架构还是单页应用,理解 JWT 对于实现安全、可扩展的身份验证都至关重要。本综合指南解释了 JWT 的工作原理、结构、安全考虑因素以及生产就绪的最佳实践。
什么是 JWT?
JWT(发音为"jot")是一个开放标准(RFC 7519),它定义了一种紧凑、自包含的格式,用于在各方之间以 JSON 对象的形式安全地传输信息。该信息使用密钥(HMAC)或公钥/私钥对(RSA 或 ECDSA)进行数字签名,确保令牌的完整性和真实性。
与传统的基于会话的身份验证(服务器在内存或数据库中维护会话状态)不同,JWT 是无状态的。验证用户身份所需的所有信息都包含在令牌本身中。
JWT 的主要用例
- 身份验证:用户登录后,服务器颁发一个 JWT,客户端在后续请求中包含该令牌以证明其身份
- 授权:JWT 可以携带用户角色、权限和范围,允许服务器在不查询数据库的情况下做出细粒度的访问控制决策
- 信息交换:JWT 的签名特性确保发送者是其声称的身份,并且数据在传输过程中未被篡改
- 单点登录(SSO):JWT 实现跨多个域和服务的无缝身份验证
- API 身份验证:移动应用和第三方集成可以在不维护服务器端会话的情况下验证 API 请求
专业提示:使用我们的 JWT 解码器在开发过程中检查和验证令牌。它可以帮助您理解结构并在问题进入生产环境之前发现常见问题。
JWT 结构详解
JWT 由三个不同的部分组成,用点(.)分隔:header.payload.signature
完整的 JWT 如下所示:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkphbmUgRGV2ZWxvcGVyIiwiZW1haWwiOiJqYW5lQGV4YW1wbGUuY29tIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNzEwNTkwNDAwLCJleHAiOjE3MTA2NzY4MDB9.4Adcj0mYZ8s5vxjKvV8pF7jKX9s8vZ5xJ3kL9mN2pQ4
第一部分:头部
头部通常包含两个描述令牌加密操作的字段:
{
"alg": "HS256",
"typ": "JWT"
}
alg指定签名算法(HS256、RS256、ES256 等)typ声明令牌类型,始终为"JWT"
此 JSON 对象经过 Base64Url 编码,形成 JWT 的第一部分。编码使其 URL 安全且紧凑,便于在 HTTP 头中传输。
第二部分:载荷
载荷包含声明——关于用户和附加元数据的陈述。声明分为三种类型:
{
"sub": "1234567890",
"name": "Jane Developer",
"email": "[email protected]",
"role": "admin",
"iat": 1710590400,
"exp": 1710676800
}
注册声明由 JWT 规范预定义,提供标准信息:
iss(issuer,颁发者):标识谁颁发了令牌sub(subject,主题):标识令牌的主题(通常是用户 ID)aud(audience,受众):标识预期接收者exp(expiration time,过期时间):令牌过期的 Unix 时间戳nbf(not before,生效时间):令牌在此时间之前无效iat(issued at,颁发时间):令牌创建时间jti(JWT ID):令牌的唯一标识符
公共声明是应在 IANA JSON Web Token 注册表中定义或使用防冲突名称(如 URL)的自定义声明。
私有声明是各方之间约定的自定义声明,如 role、permissions 或 organizationId。
重要提示:载荷仅经过 Base64Url 编码,未加密。切勿在 JWT 载荷中存储敏感信息,如密码、信用卡号或社会保险号。
第三部分:签名
签名确保令牌未被更改。它通过获取编码的头部、编码的载荷、密钥,并应用头部中指定的算法来创建:
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
签名允许接收者验证发送者是其声称的身份,以及消息在传输过程中未被更改。
签名算法比较
| 算法 | 类型 | 安全级别 | 用例 | 密钥管理 |
|---|---|---|---|---|
| HS256 | 对称(HMAC) | 良好 | 单一服务、内部 API | 共享密钥 |
| RS256 | 非对称(RSA) | 很好 | 多服务、公共 API | 公钥/私钥对 |
| ES256 | 非对称(ECDSA) | 优秀 | 高安全性要求 | 公钥/私钥对 |
| PS256 | 非对称(RSA-PSS) | 优秀 | 现代应用 | 公钥/私钥对 |
JWT 身份验证工作原理
理解完整的身份验证流程有助于您正确实现 JWT 并有效排查问题。
完整的身份验证流程
- 用户登录:客户端向身份验证端点发送凭据(用户名/密码)
- 凭据验证:服务器根据数据库验证凭据
- 令牌生成:身份验证成功后,服务器创建包含用户信息和声明的 JWT
- 令牌交付:服务器将 JWT 发送回客户端(通常在响应正文中)
- 令牌存储:客户端存储 JWT(在内存、localStorage 或 httpOnly cookie 中)
- 已验证请求:对于后续请求,客户端在 Authorization 头中包含 JWT
- 令牌验证:服务器验证 JWT 签名并检查过期时间
- 授予访问:如果有效,服务器使用令牌中的声明处理请求
身份验证请求示例
POST /api/auth/login HTTP/1.1
Host: api.example.com
Content-Type: application/json
{
"email": "[email protected]",
"password": "securePassword123"
}
身份验证响应示例
HTTP/1.1 200 OK
Content-Type: application/json
{
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"expiresIn": 3600,
"tokenType": "Bearer"
}
已验证 API 请求示例
GET /api/users/profile HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
JWT 与基于会话的身份验证对比
在 JWT 和基于会话的身份验证之间进行选择取决于应用的架构、规模和需求。
| 方面 | JWT | 基于会话 |
|---|---|---|
| 状态 | 无状态(服务器不存储令牌) | 有状态(服务器存储会话数据) |
| 可扩展性 | 优秀(无需共享存储) | 需要会话存储(Redis、数据库) |
| 撤销 | 困难(需要黑名单或短期过期) | 容易(从存储中删除会话) |
| 大小 | 较大(每次请求都发送) | 较小(仅会话 ID) |
| 跨域 | 容易(CORS 友好) | 复杂(cookie 域限制) |
| 移动应用 | 理想(无需 cookie 支持) | 具有挑战性(cookie 处理) |
| 安全性 | 需要仔细实现 | 成熟的模式 |
何时使用 JWT
- 构建需要共享身份验证的微服务
- 开发移动应用或 SPA
- 为第三方集成创建公共 API
- 跨多个域实现单点登录(SSO)
- 在不使用共享会话存储的情况下水平扩展
何时使用会话
- 构建传统的服务器渲染 Web 应用
- 需要立即撤销令牌的能力
- 处理需要服务器端控制的敏感数据
- 最小化带宽(会话使用较小的 cookie)
- 实现复杂的会话管理功能
在应用中实现 JWT
让我们通过流行编程语言和框架中的实际实现示例来演示。
Node.js 与 Express
const jwt = require('jsonwebtoken');
const express = require('express');
const app = express();
const SECRET_KEY = process.env.JWT_SECRET;
// 登录端点
app.post('/api/auth/login', async (req, res) => {
const { email, password } = req.body;
// 验证凭据(伪代码)
const user = await verifyCredentials(email, password);
if (!user) {
return res.status(401).json({ error: '凭据无效' });
}
// 生成 JWT
const token = jwt.sign(
{
sub: user.id,
email: user.email,
role: user.role
},
SECRET_KEY,
{ expiresIn: '1h' }
);
res.json({ accessToken: token });
});
// 身份验证中间件
const authenticateToken = (req, res, next) => {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
return res.status(401).json({ error: '未提供令牌' });
}
jwt.verify(token, SECRET_KEY, (err, user) => {
if (err) {
return res.status(403).json({ error: '令牌无效' });
}
req.user = user;
next();
});
};
// 受保护的路由
app.get('/api/users/profile', authenticateToken, (req, res) => {
res.json({ user: req.user });
});
Python 与 Flask
from flask import Flask, request, jsonify
import jwt
import datetime
import os
app = Flask(__name__)
SECRET_KEY = os.getenv('JWT_SECRET')
@app.route('/api/auth/login', methods=['POST'])
def login():
data = request.get_json()
email = data.get('email')
password = data.get('password')
# 验证凭据
user = verify_credentials(email, password)
if not user:
return jsonify({'error': '凭据无效'}), 401
# 生成 JWT
token = jwt.encode({
'sub': user['id'],
'email': user['email'],
'role': user['role'],
'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=1)
}, SECRET_KEY, algorithm='HS256')
return jsonify({'accessToken': token})
def token_required(f):
def decorator(*args, **kwargs):
token = request.headers.get('Authorization')
if not token:
return jsonify({'error': '未提供令牌'}), 401
try:
token = token.split(' ')[1]
data = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])
request.user = data
except jwt.ExpiredSignatureError:
return jsonify({'error': '令牌已过期'}), 403
except jwt.InvalidTokenError:
return jsonify({'error': '令牌无效'}), 403
return f(*args, **kwargs)
decorator.__name__ = f.__name__
return decorator
@app.route('/api/users/profile')
@token_required
def profile():
return jsonify({'user': request.user})
快速提示:始终使用环境变量存储密钥。切勿在源代码中硬编码或将其提交到版本控制。使用 密码生成器等工具创建强密钥。