2. JWT签发与验证流程:服务端签发JWT、客户端存储与携带、服务端验签与解析的完整生命周期
JWT(JSON Web Token)的完整生命周期涵盖三个核心阶段:服务端签发、客户端存储与携带、服务端验签与解析。理解这一流程是构建安全权限体系的基础,任何环节的疏漏都可能导致令牌泄露、伪造或权限越界。
2.1 服务端签发JWT
签发阶段发生在用户成功完成身份认证(如密码验证、OAuth授权)之后。服务端负责构造JWT的三部分结构:Header(头部)、Payload(载荷)、Signature(签名)。
| 组成部分 | 字段示例 | 说明 |
|---|---|---|
| Header | {"alg":"HS256","typ":"JWT"} |
声明签名算法(HS256/RS256)与令牌类型 |
| Payload | {"sub":"user_123","role":"admin","iat":1700000000,"exp":1700003600} |
包含用户标识、权限声明、签发时间(iat)、过期时间(exp)等 |
| Signature | HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret) | 使用服务端密钥对前两部分进行签名,防止篡改 |
关键原则:Payload中绝对不要存放密码、密钥等敏感信息。JWT的Payload仅做Base64Url编码,并非加密,任何持有令牌的人都可以解码读取。
签发时的算法选择直接影响安全性与性能:
- HS256(对称算法):单密钥,签发与验签使用同一密钥,适合单体应用或内部微服务。密钥需严格保管,泄露即全量失效。
- RS256(非对称算法):私钥签发、公钥验签,适合跨服务、跨组织的分布式场景。私钥仅签发服务持有,公钥可安全分发。
// 示例:Node.js 使用 jsonwebtoken 库签发 RS256 JWT
const jwt = require('jsonwebtoken');
const fs = require('fs');
const privateKey = fs.readFileSync('private.pem');
const payload = {
sub: 'user_123',
role: 'admin',
permissions: ['read:orders', 'write:orders'],
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + 3600 // 1小时过期
};
const token = jwt.sign(payload, privateKey, { algorithm: 'RS256' });
console.log('签发JWT:', token);
2.2 客户端存储与携带
客户端收到JWT后,需要安全存储并在后续请求中携带。存储方式直接决定令牌的抗攻击能力。
| 存储方式 | 安全性 | 适用场景 | 风险说明 |
|---|---|---|---|
| HttpOnly Cookie | 高 | Web应用(同源策略) | 防止XSS窃取,但易受CSRF攻击,需配合SameSite/CSRF Token |
| localStorage | 中 | SPA单页应用 | 易被XSS脚本读取,不推荐存储敏感令牌 |
| 内存变量 | 高 | 原生移动端/桌面端 | 页面刷新后丢失,需配合刷新令牌机制 |
携带方式通常通过HTTP Header实现:
// 标准携带方式:Authorization Header
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyXzEyMyIsInJvbGUiOiJhZG1pbiJ9.abc123_signature
// 或通过Cookie(需设置HttpOnly + Secure + SameSite=Lax)
Set-Cookie: jwt_token=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=3600
避坑提示:禁止将JWT放在URL查询参数中携带。URL会被服务器日志、浏览器历史记录、Referer Header等多处记录,极易泄露令牌。
2.3 服务端验签与解析
服务端收到请求后,需完成验签、解析、权限校验三步。验签失败或令牌过期应立即拒绝请求。
// 示例:Node.js 使用公钥验签并解析
const jwt = require('jsonwebtoken');
const fs = require('fs');
const publicKey = fs.readFileSync('public.pem');
const token = req.headers.authorization?.split(' ')[1]; // 提取Bearer Token
try {
const decoded = jwt.verify(token, publicKey, { algorithms: ['RS256'] });
// decoded 包含 payload 中的 sub, role, permissions, iat, exp 等
req.user = decoded;
next(); // 进入后续路由
} catch (err) {
if (err.name === 'TokenExpiredError') {
return res.status(401).json({ error: '令牌已过期,请刷新' });
}
if (err.name === 'JsonWebTokenError') {
return res.status(401).json({ error: '令牌无效或签名不匹配' });
}
return res.status(500).json({ error: '验签异常' });
}
验签后的解析结果应包含以下关键校验点:
- 过期时间(exp):当前时间 > exp 则拒绝,返回401。
- 签发时间(iat):可配合nbf(Not Before)字段,限制令牌在特定时间后才生效。
- 颁发者(iss):验证令牌是否由可信服务签发,防止跨域令牌滥用。
- 受众(aud):验证令牌是否指定给当前服务,防止令牌被重放到其他服务。
架构建议:在微服务架构中,建议将验签逻辑封装为网关层中间件或独立认证服务。业务服务只信任网关传递的已解析用户信息,避免每个服务重复实现验签逻辑,降低密钥分发复杂度。
2.4 完整生命周期流程图
下图展示了JWT从签发到验签的完整数据流,包含客户端、服务端、密钥存储三个角色的交互关系:
2.5 令牌刷新与黑名单机制
JWT一旦签发,在过期前无法被服务端主动撤销。为解决令牌泄露或用户权限变更问题,需引入刷新令牌与黑名单机制:
- Access Token(短期):有效期通常为15分钟~1小时,用于业务请求鉴权。
- Refresh Token(长期):有效期通常为7~30天,仅用于获取新的Access Token,存储在服务端或HttpOnly Cookie中。
- 黑名单(Blacklist):将需要撤销的JWT的jti(JWT ID)存入Redis等缓存,设置TTL与令牌过期时间一致。每次验签时检查黑名单。
// 黑名单校验伪代码
function verifyToken(token) {
const decoded = jwt.verify(token, publicKey, { algorithms: ['RS256'] });
// 检查黑名单
const isBlacklisted = await redis.get(`blacklist:${decoded.jti}`);
if (isBlacklisted) {
throw new Error('令牌已被撤销');
}
return decoded;
}
// 撤销令牌(用户登出/权限变更时调用)
async function revokeToken(jti, exp) {
const ttl = exp - Math.floor(Date.now() / 1000);
if (ttl > 0) {
await redis.setex(`blacklist:${jti}`, ttl, 'revoked');
}
}
架构级建议:在分布式系统中,黑名单应使用Redis集群或集中式缓存存储,避免每个服务实例维护独立黑名单导致不一致。同时,黑名单的TTL必须与JWT的exp对齐,过期后自动清理,防止缓存膨胀。
2.6 常见安全风险与防御
| 风险类型 | 攻击方式 | 防御措施 |
|---|---|---|
| 令牌泄露 | XSS窃取localStorage中的JWT | 使用HttpOnly Cookie存储;开启CSP(内容安全策略) |
| 令牌重放 | 截获JWT后冒充用户请求 | 使用HTTPS;设置短过期时间;绑定客户端指纹(如User-Agent+IP) |
| 算法混淆 | 将RS256改为HS256,用公钥作为密钥伪造签名 | 验签时强制指定算法白名单,拒绝alg: none |
| 密钥泄露 | 私钥或HS256密钥被窃取 | 定期轮换密钥;使用KMS管理;支持多密钥版本并行验证 |
JWT的生命周期管理是权限体系的第一道防线。签发时严格限定Payload内容与过期时间,存储时优先选择HttpOnly Cookie,验签时强制算法校验与黑名单检查,才能构建一个可信、可撤销、可审计的令牌系统。