JWT(JSON Web Token)是现代 Web 应用中最广泛使用的身份验证与信息交换标准之一。本文从原理到实践,系统性地介绍 JWT 的方方面面。
一、什么是 JWT?
JWT(JSON Web Token)是一种开放标准(RFC 7519),定义了一种紧凑且自包含的方式,以 JSON 对象的形式在各方之间安全地传输信息。由于数字签名的存在,这些信息是可验证且可信的。
两种核心使用场景:
授权(Authorization):这是 JWT 最常见的用途。用户登录后,每个后续请求都携带 JWT,允许用户访问令牌许可的路由、服务和资源。SSO(Single Sign-On)大量使用 JWT,因为其开销很小,且可跨域使用。
信息交换(Information Exchange):JWT 是在各方之间安全传输信息的好方法。由于可以对 JWT 签名(例如使用公/私钥对),你可以确认发送者的身份。此外,由于签名是用 header 和 payload 计算的,还可以验证内容未被篡改。
二、JWT 的结构
JWT 由三部分组成,用点号(.)分隔:
Header.Payload.Signature
一个真实的 JWT 看起来像这样:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
```### 2.1 Header(头部)
Header 通常由两部分组成:令牌的类型(`typ`,即 `JWT`)以及所使用的签名算法(`alg`),例如 HMAC SHA256 或 RSA。
```json
{
"alg": "HS256",
"typ": "JWT"
}
然后对该 JSON 进行 Base64URL 编码,形成 JWT 的第一部分。
2.2 Payload(载荷)
Payload 包含声明(Claims)。声明是关于实体(通常是用户)及附加数据的陈述。JWT 定义了三种类型的声明:
注册声明(Registered Claims)------预定义的、非强制性但推荐使用的声明集:
| 字段 | 全称 | 说明 |
|---|---|---|
iss |
Issuer | 签发方 |
sub |
Subject | 主题(通常为用户 ID) |
aud |
Audience | 接收方 |
exp |
Expiration Time | 过期时间(Unix 时间戳) |
nbf |
Not Before | 生效时间 |
iat |
Issued At | 签发时间 |
jti |
JWT ID | JWT 唯一标识,可防重放 |
公共声明(Public Claims)------由使用者自由定义,但应在 IANA JSON Web Token Registry 中注册或使用带命名空间的 URI,以避免冲突。
私有声明(Private Claims) ------通信双方约定的自定义字段,例如 "role": "admin" 或 "permissions": ["read", "write"]。
json
{
"sub": "1234567890",
"name": "Alice",
"role": "admin",
"iat": 1516239022,
"exp": 1516242622
}
重要 :Payload 是经过 Base64URL 编码的,而非加密的。任何持有令牌的人都可以解码并读取内容。绝对不要将密码、敏感个人信息等放入 Payload。
2.3 Signature(签名)
签名用于验证消息在传输过程中未被篡改,对于使用私钥签名的令牌,还可以验证 JWT 的发送者身份。
以 HS256 算法为例,签名的计算方式如下:
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
三、JWT 的工作流程整体流程分为两个阶段:认证阶段 (一次性)和授权阶段(每次请求)。
在实际 HTTP 请求中,JWT 通常放在 Authorization 请求头中,格式为:
http
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
四、签名算法详解
JWT 支持多种签名算法,选择哪种取决于你的安全需求和架构模式。
4.1 对称算法(HMAC)
使用同一个密钥进行签名和验证。
HS256 → HMAC + SHA-256
HS384 → HMAC + SHA-384
HS512 → HMAC + SHA-512
适用场景:签发方与验证方是同一个服务(例如单体应用,或内部微服务之间的信任通信)。
风险:所有需要验证 JWT 的服务都必须持有密钥,一旦泄露,所有令牌都可伪造。
4.2 非对称算法(RSA / ECDSA)
使用私钥签名,公钥验证。
RS256 → RSA + SHA-256(最常用)
RS384 / RS512
ES256 → ECDSA + P-256(更小的密钥,更快的计算)
ES384 / ES512
适用场景:Auth Server 持有私钥签发令牌,各 Resource Server 只需公钥即可验证,无需信任链扩散。这是微服务架构中的推荐方案。
私钥(Auth Server 独有)→ 签名 JWT
公钥(所有 Resource Server 共享)→ 验证签名
五、实战代码示例
5.1 Node.js(jsonwebtoken)
javascript
const jwt = require('jsonwebtoken');
// 签发 JWT
function issueToken(userId, role) {
const payload = {
sub: userId,
role: role,
iat: Math.floor(Date.now() / 1000),
};
return jwt.sign(payload, process.env.JWT_SECRET, {
algorithm: 'HS256',
expiresIn: '2h', // 过期时间
issuer: 'my-app',
audience: 'my-api',
});
}
// 验证 JWT
function verifyToken(token) {
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET, {
algorithms: ['HS256'], // 白名单,防止 alg:none 攻击
issuer: 'my-app',
audience: 'my-api',
});
return { ok: true, payload: decoded };
} catch (err) {
return { ok: false, error: err.message };
}
}
// Express 中间件示例
function authMiddleware(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Missing token' });
}
const token = authHeader.slice(7);
const result = verifyToken(token);
if (!result.ok) {
return res.status(401).json({ error: result.error });
}
req.user = result.payload; // 注入用户信息
next();
}
5.2 Python(PyJWT)
python
import jwt
import os
from datetime import datetime, timedelta, timezone
SECRET_KEY = os.environ["JWT_SECRET"]
ALGORITHM = "HS256"
# 签发 JWT
def create_token(user_id: str, role: str) -> str:
payload = {
"sub": user_id,
"role": role,
"iat": datetime.now(timezone.utc),
"exp": datetime.now(timezone.utc) + timedelta(hours=2),
"iss": "my-app",
}
return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)
# 验证 JWT
def verify_token(token: str) -> dict:
try:
payload = jwt.decode(
token,
SECRET_KEY,
algorithms=[ALGORITHM], # 白名单
issuer="my-app",
)
return {"ok": True, "payload": payload}
except jwt.ExpiredSignatureError:
return {"ok": False, "error": "Token expired"}
except jwt.InvalidTokenError as e:
return {"ok": False, "error": str(e)}
六、安全最佳实践
这是 JWT 开发中最容易出错的地方。以下问题在真实漏洞报告中反复出现。
6.1 算法混淆攻击(alg:none)
危险代码:
javascript
// ❌ 危险:信任 Header 中的 alg 字段
const decoded = jwt.verify(token, secret);
攻击者可以将 Header 中的 alg 改为 none,然后发送一个没有签名的令牌,部分库会错误地接受它。
正确做法:
javascript
// ✅ 安全:在验证时显式指定允许的算法白名单
const decoded = jwt.verify(token, secret, { algorithms: ['HS256'] });
6.2 RS256/HS256 混淆攻击
若服务端使用 RS256,攻击者可以将 alg 改为 HS256,并用服务端的公钥(通常是公开的)作为 HMAC 密钥来签名伪造令牌。始终在验证时硬编码期望的算法,而不是读取令牌 Header 中的算法。
6.3 密钥强度
HMAC 密钥应至少 256 位(32 字节),且随机生成,绝不使用短字符串或常见词组:
bash
# 生成强随机密钥
node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"
6.4 过期时间(exp)
永远要设置过期时间。Access Token 建议 15 分钟到 2 小时,Refresh Token 可以更长(7 到 30 天)。
javascript
jwt.sign(payload, secret, { expiresIn: '15m' }); // 短期令牌
6.5 令牌存储
| 存储位置 | XSS 风险 | CSRF 风险 | 推荐度 |
|---|---|---|---|
localStorage |
高(JS 可读) | 无 | ❌ 不推荐 |
sessionStorage |
高(JS 可读) | 无 | ❌ 不推荐 |
| 内存(JS 变量) | 低 | 无 | ✅ 推荐 |
HttpOnly Cookie |
无(JS 不可读) | 需配合 CSRF Token | ✅ 推荐 |
6.6 不可撤销性问题与解决方案
JWT 的一大设计特点是无状态------服务端不存储令牌,因此无法主动使其失效。这在以下场景下成为问题:
- 用户主动登出
- 账号被封禁
- 权限变更需立即生效
常用解决方案:
方案一:短过期时间 + Refresh Token 机制
Access Token → 15 分钟过期(泄露影响窗口小)
Refresh Token → 存储在服务端,支持主动吊销
方案二:Token 黑名单(Blocklist)
Redis 存储已吊销的 jti,验证时检查(牺牲部分无状态性)
方案三:Token 版本号
用户表中存储 token_version,每次登出自增
JWT 中携带版本号,验证时与数据库比对
七、JWT vs Session:如何选择?
| 维度 | JWT | Session |
|---|---|---|
| 状态 | 无状态,服务端不存储 | 有状态,服务端存储 Session |
| 水平扩展 | 天然支持,无需共享存储 | 需 Session 共享(Redis 等) |
| 令牌吊销 | 困难(需额外机制) | 简单(直接删除 Session) |
| 跨域/跨服务 | 天然支持(Authorization Header) | 受 Cookie 同源限制 |
| 网络开销 | 每次请求携带令牌(较大) | Cookie 较小 |
| 微服务/SSO | 非常适合 | 较难实现 |
| 即时权限变更 | 需要额外设计 | 立即生效 |
选择建议:微服务架构、需要 SSO(Single Sign-On)的场景、无状态 API,倾向 JWT;传统 Web 应用、需要立即吊销会话、更严格的安全控制,倾向 Session 或两者结合(使用 JWT 作为 Refresh Token 的凭证,Session 作为短期授权)。
八、Refresh Token 模式
生产环境中,通常结合使用短期 Access Token 和长期 Refresh Token:
登录成功
↓
返回:
access_token(JWT,15 分钟过期)
refresh_token(不透明随机字符串,30 天,存储在 HttpOnly Cookie)
访问资源:携带 access_token
↓ 若 access_token 过期(401)
↓
用 refresh_token 换取新 access_token(POST /auth/refresh)
→ Auth Server 验证 refresh_token(查库)
→ 若有效,颁发新 access_token(可选:Rotation,同时更新 refresh_token)
↓
继续使用新 access_token
Refresh Token Rotation(轮换)是一种重要的安全机制:每次使用 Refresh Token 时,同时使旧令牌失效并颁发新令牌,若检测到旧令牌被重复使用,则认为令牌已被盗,立即吊销所有相关令牌。
九、常见误区总结
误区一:JWT 是加密的。 JWT 默认只做签名(JWS),不加密。Payload 是 Base64URL 编码,任何人都可以解码读取。若需加密,使用 JWE(JSON Web Encryption)。
误区二:JWT 比 Session 更安全。 JWT 和 Session 各有优劣,安全性取决于正确实现,不是 JWT 本身更安全。
误区三:在 JWT 中存储密码或敏感数据。 绝不这样做。
误区四:JWT 可以无限使用。 一定要设置合理的过期时间,并考虑令牌吊销机制。
误区五:alg: none 被某些库接受是小事。 这是一个严重的安全漏洞,一定要在验证时白名单指定算法。
JWT 凭借其无状态、自包含、跨语言的特性,成为了现代 API 认证的事实标准。但使用得当的关键在于理解它的设计边界:JWT 解决了"验证信息的真实性与完整性"的问题,而"令牌吊销"、"存储安全"等问题需要在架构层面补充解决。掌握这些,才能将 JWT 用好、用安全。