JWT(JSON Web Token)是当代 API 鉴权的事实标准。微服务之间互信、单点登录、API Key、OAuth2 的 access_token —— 全是 JWT 或其变种。它的"无状态、自包含"特性让分布式鉴权变得简单。但 JWT 也是被误用最严重的技术之一,"把 JWT 当 Session 用" / "用 none 算法" / "jwt 永不过期" 这类反模式比比皆是。这篇文章把 JWT 讲透,涵盖结构、签名、安全坑、生产实践。
JWT 的结构
一个 JWT 长这样,用 . 分成三段:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NSIsIm5hbWUiOiJtb3JlcyJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
# 三段:
# Header.Payload.Signature
# Header(base64url 解码)
{
"alg": "HS256",
"typ": "JWT"
}
# Payload(base64url 解码)—— 注意这是公开可读的!
{
"sub": "12345", # subject,通常是用户 ID
"name": "mores",
"iat": 1700000000, # issued at
"exp": 1700003600 # expiration
}
# Signature
HMAC_SHA256(
base64url(Header) + "." + base64url(Payload),
secret_key
)
核心特点
- 自包含:用户信息在 token 里,服务端无需查库。
- 可验证:签名保证内容没被篡改。
- 无状态:服务端不存 token,扩容简单。
- 可读不可改:Payload 是 base64,任何人能解码看内容;但改了签名验证失败。
标准 Claims
RFC 7519 定义的标准字段:
iss Issuer 颁发者(如 "https://auth.myapp.com")
sub Subject 主题(通常用户 ID)
aud Audience 接收方(API 服务 ID,验证用)
exp Expiration 过期时间(unix timestamp)
iat Issued At 签发时间
nbf Not Before 生效时间(可以让 token "未来才有效")
jti JWT ID 唯一 ID,可用于撤销
# 自定义 claim 加在上面:
{
"iss": "https://auth.myapp.com",
"sub": "user_123",
"aud": "https://api.myapp.com",
"exp": 1700003600,
"role": "admin",
"tenant_id": "company_42"
}
签名算法
HS256(对称)
HMAC + SHA256,用一个 secret 既签名又验证。简单,但验证方需要知道 secret —— 不适合多方场景。
// 签
const token = jwt.sign({sub: 'u1'}, SECRET, { algorithm: 'HS256' });
// 验
const decoded = jwt.verify(token, SECRET, { algorithms: ['HS256'] });
RS256(非对称,推荐)
RSA + SHA256,用私钥签名,公钥验证。颁发方独占私钥,所有验证方持公钥。多服务场景的首选。
const token = jwt.sign({sub: 'u1'}, PRIVATE_KEY, { algorithm: 'RS256' });
const decoded = jwt.verify(token, PUBLIC_KEY, { algorithms: ['RS256'] });
ES256(椭圆曲线,现代)
ECDSA + SHA256,功能同 RS256 但密钥更短(P-256),签名更快。新项目优先 ES256。
禁用 none 算法
JWT 标准里有个 "alg": "none"(不签名)选项。某些库默认接受 —— 攻击者构造 none 的 token 直接通过验证!验证时必须显式指定允许的算法:
# 危险
jwt.decode(token); # 不验证,只解析
jwt.verify(token, secret); # 某些老版本可能接受 none
# 安全
jwt.verify(token, secret, { algorithms: ['RS256'] }); # 显式指定算法
JWT 的常见误用
误用 1:用 JWT 当 Session
JWT 无状态意味着颁发后没法立刻撤销。用户登出 / 改密码 / 被封号后,旧 token 在过期前仍然有效。如果 token 过期时间设 7 天,被盗号后这 7 天都防不住。
正确用法:
- access_token 短期(15 分钟 - 2 小时),配 refresh_token。
- 关键场景查黑名单:登出 / 改密码时把 jti 加入 Redis 黑名单,验证时查一下。
- 真的需要可控的会话:用传统 Session(存 Redis),把 session_id 给客户端。
误用 2:把敏感数据放 Payload
Payload 是 base64,谁都能解码看。不要放密码、信用卡号、隐私数据。只放鉴权必要的标识(用户 ID、role、tenant_id)。
误用 3:token 永不过期
"为了用户体验,token 设 1 年" —— token 泄漏了 1 年都防不住。规则:access_token ≤ 1 小时,refresh_token 短到 30 天。
误用 4:secret 太弱
HS256 的 secret 是字符串,如果短或简单,可以被暴力破解(JWT cracker 工具几分钟搞定常见弱密码)。secret 至少 256 位随机:
openssl rand -base64 32
Access Token + Refresh Token 模式
登录返回:
{
"access_token": "eyJ...", // 15 分钟过期,带在每个 API 请求
"refresh_token": "eyJ...", // 30 天,只用来换新 access_token
}
# 客户端
- access_token 放内存(JS 变量),不放 localStorage(防 XSS)
- refresh_token 放 HttpOnly Cookie(防 XSS)
- 收到 401 时,用 refresh_token 调 /auth/refresh 拿新 access_token,再重试请求
# refresh_token rotation(强烈推荐)
每次用 refresh_token 换新 access_token 时,同时换一个新 refresh_token
旧 refresh_token 立刻失效。攻击者拿到旧的也没用,
而且如果旧的被使用过,说明可能被盗 -> 触发安全告警。
JWKS:动态获取公钥
RS256 签发的 JWT,验证方怎么知道用哪个公钥?用 JWKS(JSON Web Key Set):
# 颁发者公开 .well-known/jwks.json:
GET https://auth.myapp.com/.well-known/jwks.json
{
"keys": [
{
"kty": "RSA",
"kid": "key1", // 密钥 ID
"use": "sig",
"n": "...", "e": "AQAB" // 公钥参数
},
{
"kty": "RSA",
"kid": "key2", // 新密钥,用于密钥轮换
...
}
]
}
# JWT 的 Header 带 kid,告诉验证方用哪个公钥
{ "alg": "RS256", "typ": "JWT", "kid": "key1" }
# 验证流程:
1. 取 token 的 kid
2. 从 JWKS 找对应公钥(可缓存)
3. 用该公钥验证签名
这套机制让颁发方可以定期轮换密钥(发布新 kid,新 token 用新 kid 签,旧 token 仍能用旧 kid 验,直到过期 + 公钥下线)。
实战:Node.js 实现
const jwt = require('jsonwebtoken');
const fs = require('fs');
const privateKey = fs.readFileSync('./keys/private.pem');
const publicKey = fs.readFileSync('./keys/public.pem');
// 签发(登录成功后)
function issueTokens(user) {
const accessToken = jwt.sign(
{ sub: user.id, role: user.role },
privateKey,
{
algorithm: 'RS256',
issuer: 'https://auth.myapp.com',
audience: 'https://api.myapp.com',
expiresIn: '15m',
}
);
const refreshToken = jwt.sign(
{ sub: user.id, jti: crypto.randomUUID() },
privateKey,
{ algorithm: 'RS256', expiresIn: '30d' }
);
return { accessToken, refreshToken };
}
// 验证中间件
function authMiddleware(req, res, next) {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) return res.status(401).end();
try {
const payload = jwt.verify(token, publicKey, {
algorithms: ['RS256'],
issuer: 'https://auth.myapp.com',
audience: 'https://api.myapp.com',
});
req.user = { id: payload.sub, role: payload.role };
next();
} catch (e) {
return res.status(401).json({ error: e.message });
}
}
JWT 黑名单
需要"立刻撤销 token"时,用 Redis 黑名单:
// 登出
function logout(req, res) {
const token = req.headers.authorization.replace('Bearer ', '');
const payload = jwt.decode(token);
const ttl = payload.exp - Math.floor(Date.now() / 1000);
redis.setEx(`bl:${payload.jti}`, ttl, '1');
}
// 验证中间件加一步
async function authMiddleware(req, res, next) {
// ... 先验签名
if (await redis.get(`bl:${payload.jti}`)) {
return res.status(401).json({ error: 'token revoked' });
}
// ...
}
JWT vs Session vs PASETO
- JWT:无状态、可跨服务,但撤销难、payload 暴露。
- Session:有状态、立刻撤销,但需要中心存储、不能跨服务。
- PASETO:JWT 的"安全替代品",修复 JWT 标准里几个安全缺陷(算法混淆、none 算法等),设计上"不可滥用"。
2025 年的主流仍然是 JWT —— 因为生态成熟、各语言库齐全。但要严格按最佳实践用。
常见安全问题
问题 1:Algorithm Confusion 攻击。服务用 RS256 验证,但传 "alg": "HS256" 的 token,部分库会用公钥作为 HMAC 密钥验证 —— 公钥是公开的,攻击者能伪造任意 token。修复:验证时严格指定 algorithms 数组。
问题 2:kid 注入。kid 来自 token 本身(用户可控),如果服务端拿 kid 去 SQL 查询或文件读取,可能引发 SQL 注入或路径遍历。修复:kid 当作枚举值验证,不能用作数据源标识符。
问题 3:JWT 过大。塞太多数据,header 几 KB,每个请求都带,流量浪费。规则:Payload 只放必要鉴权信息,详细数据每次查库。
JWT 在微服务里的传递
JWT 的核心价值在微服务架构里体现得最明显:
客户端 -> API Gateway -> Service A -> Service B -> Service C
# 每一跳都带着同一个 JWT(或者 Gateway 重新签发服务间 token)
# 每个服务用同一组公钥验证 JWT
# 不需要中央 Session 服务,验证完全本地
关键设计:
- JWKS endpoint 缓存:每个服务启动时拉一次公钥,后续按 TTL 刷新(避免每次验证都查 IdP)。
- 用 audience 区分服务:Service A 的 token 不应被 Service B 接受 —— 即使签名验证通过,aud 不对就拒。
- 异步任务带 token:消息里带 JWT,消费方异步处理时仍能验证身份。
JWT 大小限制
HTTP header 总大小通常被限制在 4-16KB,JWT 占了大头要小心。
# 不要在 JWT 塞:
- 用户的所有 profile 字段
- 权限列表(可能几十条)
- 设备信息、IP 历史
# 应该:
- 用户 ID
- 简化的 role(admin / user / vip)
- 必要的 tenant_id 等隔离字段
# 详细信息从这些 ID 现查
JWT 与 CORS
跨域调 API 带 Authorization header,浏览器会做预检请求(OPTIONS)。服务端要正确响应:
Access-Control-Allow-Origin: https://yourapp.com
Access-Control-Allow-Credentials: true
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Max-Age: 86400
注意:Allow-Credentials: true 时 Origin 不能用 *,必须显式指定。
JWT 调试工具
- jwt.io:在线解析 + 验证 JWT,贴 token 上去看 header/payload/signature。
- jwt-cli:命令行版,适合 CI / 脚本。
- Postman:OAuth2 Helper,自动管理 token 生命周期。
注意:不要把生产环境的真 token 贴 jwt.io —— 它在浏览器里解析,理论上不上传,但敏感场景仍要小心。
写在最后
JWT 是优秀的工具但极易被误用。把它定位准确:跨服务、短期、自包含的身份凭证。该撤销时配黑名单,该长存时改 refresh_token + 短期 access_token,该多方时用 JWKS + RS256。把这些原则做到位,JWT 就是高效又安全的鉴权方案。不要为了 JWT 而 JWT —— 简单的单体应用,Session 比 JWT 简单可控,选 JWT 是给自己找麻烦。
一图看懂
JWT 认证一图看懂:
—— 别看了 · 2026