跳到主要内容

JWT 认证

JWT(JSON Web Token)是一种开放标准(RFC 7519),用于在各方之间安全地传输信息作为 JSON 对象。JWT 是目前最流行的无状态认证机制,广泛应用于分布式系统、微服务架构和单页应用(SPA)。

为什么需要 JWT?

在传统的 Session-Cookie 认证中,服务器需要存储每个用户的会话状态。这在分布式系统中带来了挑战:

  1. 会话共享问题 - 多台服务器之间需要同步会话状态
  2. 服务器负担 - 需要维护大量的会话数据
  3. 跨域限制 - 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(注册声明) - 预定义的、建议使用的声明:

声明含义说明
issIssuer签发者
subSubject主题(用户标识)
audAudience接收方
expExpiration Time过期时间(Unix 时间戳)
nbfNot Before生效时间
iatIssued At签发时间
jtiJWT 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,椭圆曲线

认证流程

登录流程

  1. 用户提交凭据 - 客户端发送用户名和密码到服务器
  2. 服务器验证 - 服务器验证凭据是否正确
  3. 生成 JWT - 验证通过后,服务器生成包含用户信息的 JWT
  4. 返回令牌 - 服务器将 JWT 返回给客户端
  5. 客户端存储 - 客户端将 JWT 存储在本地(LocalStorage、Cookie 或内存)

请求流程

  1. 携带令牌 - 客户端在请求头的 Authorization 字段中携带 JWT
    Authorization: Bearer <jwt_token>
  2. 服务器验证 - 服务器验证 JWT 的签名和过期时间
  3. 返回资源 - 验证通过后返回请求的资源

刷新机制

由于 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 对比

特性JWTSession
存储位置客户端服务器端
服务器状态无状态有状态
扩展性易于水平扩展需要共享会话存储
性能无需查询数据库需要查询会话存储
安全性令牌一旦颁发无法撤销可随时使会话失效
跨域支持天然支持需要额外配置
令牌大小较大(包含用户信息)很小(仅 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 是一种强大的无状态认证机制,适用于现代分布式系统:

  1. 核心优势

    • 无状态,易于水平扩展
    • 自包含,减少数据库查询
    • 跨域友好,适合微服务架构
  2. 安全要点

    • 使用强密钥和安全的签名算法
    • 设置合理的过期时间
    • 不在 Payload 中存储敏感信息
    • 实现令牌刷新和黑名单机制
  3. 最佳实践

    • 访问令牌短期有效(15-60 分钟)
    • 刷新令牌长期有效(7-30 天)
    • 使用 HTTPS 传输所有令牌
    • 考虑使用 HttpOnly Cookie 存储令牌