JWT 认证
JWT(JSON Web Token)是一种开放标准(RFC 7519),用于在各方之间安全地传输信息作为 JSON 对象。JWT 是目前最流行的无状态认证机制,广泛应用于分布式系统、微服务架构和单页应用(SPA)。
为什么需要 JWT?
在传统的 Session-Cookie 认证中,服务器需要存储每个用户的会话状态。这在分布式系统中带来了挑战:
- 会话共享问题 - 多台服务器之间需要同步会话状态
- 服务器负担 - 需要维护大量的会话数据
- 跨域限制 - Cookie 受同源策略限制
JWT 通过将用户状态编码在令牌本身中,实现了无状态认证:
┌─────────┐ ┌─────────┐
│ Client │ │ Server │
└────┬────┘ └────┬────┘
│ │
│ 1. 登录请求 (username, password) │
│ ───────────────────────────────────────────> │
│ │
│ 2. 验证成功,生成 JWT(包含用户信息和签名) │
│ │
│ 3. 返回 JWT │
│ <─────────────────────────────────────────── │
│ │
│ 4. 后续请求携带 JWT (Authorization Header) │
│ ───────────────────────────────────────────> │
│ 5. 验证签名和过期时间,无需查询数据库 │
│ 6. 返回受保护资源 │
│ <─────────────────────────────────────────── │
JWT 结构
JWT 由三部分组成,用点号(.)分隔:
xxxxx.yyyyy.zzzzz
↑ ↑ ↑
Header Payload Signature
Header(头部)
Header 包含令牌类型和签名算法:
{
"alg": "HS256",
"typ": "JWT"
}
- alg: 签名算法(HS256、HS512、RS256、ES256 等)
- typ: 令牌类型,通常为 JWT
Payload(载荷)
Payload 包含声明(claims),即要传输的数据:
{
"sub": "1234567890",
"name": "John Doe",
"admin": true,
"iat": 1516239022,
"exp": 1516242622
}
Registered Claims(注册声明) - 预定义的、建议使用的声明:
| 声明 | 含义 | 说明 |
|---|---|---|
iss | Issuer | 签发者 |
sub | Subject | 主题(用户标识) |
aud | Audience | 接收方 |
exp | Expiration Time | 过期时间(Unix 时间戳) |
nbf | Not Before | 生效时间 |
iat | Issued At | 签发时间 |
jti | JWT ID | 唯一标识 |
Public Claims(公共声明) - 自定义的、避免冲突的声明
Private Claims(私有声明) - 应用内部使用的自定义声明
Signature(签名)
签名用于验证消息未被篡改,并验证发送者身份:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret
)
签名算法的选择:
| 算法 | 类型 | 说明 |
|---|---|---|
| HS256 | 对称加密 | HMAC + SHA-256,密钥共享 |
| HS512 | 对称加密 | HMAC + SHA-512,密钥共享 |
| RS256 | 非对称加密 | RSA + SHA-256,私钥签名,公钥验证 |
| ES256 | 非对称加密 | ECDSA + SHA-256,椭圆曲线 |
认证流程
登录流程
- 用户提交凭据 - 客户端发送用户名和密码到服务器
- 服务器验证 - 服务器验证凭据是否正确
- 生成 JWT - 验证通过后,服务器生成包含用户信息的 JWT
- 返回令牌 - 服务器将 JWT 返回给客户端
- 客户端存储 - 客户端将 JWT 存储在本地(LocalStorage、Cookie 或内存)
请求流程
- 携带令牌 - 客户端在请求头的
Authorization字段中携带 JWTAuthorization: Bearer <jwt_token> - 服务器验证 - 服务器验证 JWT 的签名和过期时间
- 返回资源 - 验证通过后返回请求的资源
刷新机制
由于 JWT 一旦颁发无法撤销,通常采用双令牌策略:
- Access Token - 短期有效(15-60 分钟),用于访问资源
- Refresh Token - 长期有效(7-30 天),用于获取新的 Access Token
┌─────────┐ ┌─────────┐
│ Client │ │ Server │
└────┬────┘ └────┬────┘
│ │
│ Access Token 过期 (401) │
│ <────────────────────────────────────── │
│ │
│ 使用 Refresh Token 请求新令牌 │
│ ──────────────────────────────────────> │
│ │
│ 返回新的 Access Token │
│ <────────────────────────────────────── │
│ │
│ 使用新 Access Token 重试原请求 │
│ ──────────────────────────────────────> │
安全最佳实践
密钥管理
- 使用加密安全的随机数生成密钥
- 密钥长度至少 256 位(32 字节)
- 从环境变量读取密钥,不要硬编码
- 定期轮换密钥
令牌安全
- 设置合理的过期时间(Access Token: 15-60 分钟)
- 不在 Payload 中存储敏感信息(密码、身份证号等)
- 使用 HTTPS 传输所有令牌
- 考虑使用 HttpOnly Cookie 存储令牌(防止 XSS)
常见安全错误
| 错误 | 风险 | 正确做法 |
|---|---|---|
| 使用弱密钥 | 容易被暴力破解 | 使用强密钥生成器 |
| 存储敏感信息 | 信息泄露 | 只存储用户 ID 和角色 |
| 无过期时间 | 令牌永久有效 | 设置合理的过期时间 |
| LocalStorage 存储 | XSS 攻击风险 | 使用 HttpOnly Cookie |
| 不验证签名算法 | 算法切换攻击 | 明确指定允许的算法 |
JWT 与 Session 对比
| 特性 | JWT | Session |
|---|---|---|
| 存储位置 | 客户端 | 服务器端 |
| 服务器状态 | 无状态 | 有状态 |
| 扩展性 | 易于水平扩展 | 需要共享会话存储 |
| 性能 | 无需查询数据库 | 需要查询会话存储 |
| 安全性 | 令牌一旦颁发无法撤销 | 可随时使会话失效 |
| 跨域支持 | 天然支持 | 需要额外配置 |
| 令牌大小 | 较大(包含用户信息) | 很小(仅 Session ID) |
| 适用场景 | 分布式系统、移动应用 | 传统 Web 应用 |
适用场景
推荐使用 JWT
- 分布式系统和微服务架构
- 单页应用(SPA)
- 移动端应用
- 跨域 API 访问
- 第三方开放平台
推荐使用 Session
- 传统服务端渲染应用
- 需要强会话控制的系统
- 需要随时强制登出的场景
- 企业内部管理系统
代码实现
Node.js (jsonwebtoken)
const jwt = require('jsonwebtoken');
const SECRET = process.env.JWT_SECRET;
function generateTokens(user) {
const accessToken = jwt.sign(
{ userId: user.id, role: user.role },
SECRET,
{ expiresIn: '15m' }
);
const refreshToken = jwt.sign(
{ userId: user.id },
SECRET,
{ expiresIn: '7d' }
);
return { accessToken, refreshToken };
}
// 认证中间件
function authMiddleware(req, res, next) {
const token = req.headers.authorization?.split(' ')[1];
if (!token) return res.status(401).json({ error: 'No token' });
try {
req.user = jwt.verify(token, SECRET);
next();
} catch (err) {
res.status(401).json({ error: 'Invalid token' });
}
}
Python (PyJWT)
import jwt
import datetime
SECRET = os.environ['JWT_SECRET']
def generate_tokens(user):
access_token = jwt.encode({
'user_id': user.id,
'role': user.role,
'exp': datetime.datetime.utcnow() + datetime.timedelta(minutes=15)
}, SECRET, algorithm='HS256')
refresh_token = jwt.encode({
'user_id': user.id,
'exp': datetime.datetime.utcnow() + datetime.timedelta(days=7)
}, SECRET, algorithm='HS256')
return access_token, refresh_token
def verify_token(token):
try:
return jwt.decode(token, SECRET, algorithms=['HS256'])
except jwt.ExpiredSignatureError:
return None
except jwt.InvalidTokenError:
return None
Go (golang-jwt)
package auth
import (
"time"
"github.com/golang-jwt/jwt/v5"
)
var secretKey = []byte(os.Getenv("JWT_SECRET"))
func GenerateTokens(userID uint, role string) (string, string, error) {
accessClaims := jwt.MapClaims{
"user_id": userID,
"role": role,
"exp": time.Now().Add(15 * time.Minute).Unix(),
}
accessToken := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims)
accessString, err := accessToken.SignedString(secretKey)
refreshClaims := jwt.MapClaims{
"user_id": userID,
"exp": time.Now().Add(7 * 24 * time.Hour).Unix(),
}
refreshToken := jwt.NewWithClaims(jwt.SigningMethodHS256, refreshClaims)
refreshString, err := refreshToken.SignedString(secretKey)
return accessString, refreshString, err
}
小结
JWT 是一种强大的无状态认证机制,适用于现代分布式系统:
-
核心优势
- 无状态,易于水平扩展
- 自包含,减少数据库查询
- 跨域友好,适合微服务架构
-
安全要点
- 使用强密钥和安全的签名算法
- 设置合理的过期时间
- 不在 Payload 中存储敏感信息
- 实现令牌刷新和黑名单机制
-
最佳实践
- 访问令牌短期有效(15-60 分钟)
- 刷新令牌长期有效(7-30 天)
- 使用 HTTPS 传输所有令牌
- 考虑使用 HttpOnly Cookie 存储令牌