3. JWT 工作流程:客户端请求、服务端签发、客户端存储、服务端验证的完整流程
JWT(JSON Web Token)的完整工作流程贯穿了用户认证与授权的四个核心阶段:客户端发起请求、服务端签发 Token、客户端安全存储以及服务端验证 Token。理解这一闭环是掌握 JWT 权限验证的基础。
核心要点: JWT 是一种无状态(Stateless)的认证机制。服务端不保存会话信息,所有用户身份与权限数据都编码在 Token 本身,通过数字签名保证其完整性与真实性。
3.1 完整流程总览
下图展示了 JWT 从用户登录到后续请求验证的完整生命周期,包含 7 个关键步骤。
3.2 阶段一:客户端请求(登录)
流程的起点是用户通过客户端(浏览器、移动 App 等)向服务端发送登录请求。这是整个认证链条的触发动作。
- 请求方式:通常使用
POST方法,通过 HTTPS 协议传输,防止敏感信息在传输过程中被窃听。 - 请求内容:包含用户的身份凭证,最常见的是
username(或email)和password。 - 数据格式:请求体通常采用 JSON 格式,例如:
POST /api/auth/login HTTP/1.1
Host: api.example.com
Content-Type: application/json
{
"username": "alice@example.com",
"password": "securePassword123"
}
安全提示:生产环境中务必使用 HTTPS 加密传输。明文传输密码在公网上是严重的安全漏洞,极易被中间人攻击截获。
3.3 阶段二:服务端签发 JWT
服务端收到登录请求后,执行以下三个子步骤来签发 JWT:
- 验证身份:查询数据库,比对用户名和密码(密码通常使用 bcrypt 等算法哈希存储)。
- 构建 Payload:将用户标识(如
user_id)、角色(如role)、权限范围(如scope)以及签发时间(iat)、过期时间(exp)等声明写入 JWT 的 Payload 部分。 - 生成签名:使用服务端持有的密钥(Secret)或私钥(Private Key),对 Header 和 Payload 进行签名,生成最终的 JWT 字符串。
签发过程的伪代码如下:
// 服务端签发 JWT 伪代码
function signJWT(user) {
const header = { "alg": "HS256", "typ": "JWT" };
const payload = {
"sub": user.id, // 用户 ID
"name": user.username, // 用户名
"role": user.role, // 用户角色
"iat": Math.floor(Date.now() / 1000), // 签发时间
"exp": Math.floor(Date.now() / 1000) + 3600 // 过期时间(1小时后)
};
const secret = "your-256-bit-secret"; // 服务端密钥
const token = jwt.sign(header, payload, secret);
return token;
}
3.4 阶段三:客户端存储 JWT
客户端收到服务端返回的 JWT 后,需要将其安全地存储起来,以便在后续请求中携带。常见的存储方式有两种,各有优劣:
| 存储方式 | 实现方法 | 优点 | 缺点 |
|---|---|---|---|
| localStorage / sessionStorage | JavaScript 直接存储 | 简单易用,跨标签页共享(localStorage) | 易受 XSS 攻击,攻击者可通过脚本窃取 Token |
| HttpOnly Cookie | 服务端设置 Set-Cookie 头 |
无法被 JavaScript 读取,天然防御 XSS | 易受 CSRF 攻击,需额外配置 SameSite 属性 |
安全警告:避免将 JWT 存储在 URL 参数或浏览器的
window.name 中。这些位置容易被泄露或劫持。对于高安全性应用,推荐使用 HttpOnly + Secure + SameSite=Strict 的 Cookie 方案。
3.5 阶段四:服务端验证 JWT
当客户端访问受保护资源时,会在 HTTP 请求头中携带 JWT。服务端收到请求后,执行严格的验证流程:
- 提取 Token:从
Authorization: Bearer <token>头中提取 JWT 字符串。 - 解析与验签:使用服务端持有的密钥(与签发时相同)验证签名。如果签名不匹配,说明 Token 被篡改,直接拒绝请求。
- 检查有效期:验证
exp(过期时间)和nbf(生效时间)等时间声明。如果 Token 已过期,返回401 Unauthorized。 - 权限校验:从 Payload 中提取用户角色或权限列表,判断当前用户是否有权访问所请求的资源。
- 放行或拒绝:验证通过后,将用户信息注入请求上下文(如
req.user),并放行请求到业务逻辑层;验证失败则返回401或403状态码。
验证过程的伪代码如下:
// 服务端验证 JWT 伪代码(中间件)
function authMiddleware(req, res, next) {
const authHeader = req.headers['authorization'];
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: '未提供认证 Token' });
}
const token = authHeader.split(' ')[1];
try {
const decoded = jwt.verify(token, 'your-256-bit-secret');
// 检查权限(示例:仅管理员可访问)
if (decoded.role !== 'admin') {
return res.status(403).json({ error: '权限不足' });
}
req.user = decoded; // 将用户信息注入请求
next(); // 放行
} catch (err) {
if (err.name === 'TokenExpiredError') {
return res.status(401).json({ error: 'Token 已过期' });
}
return res.status(401).json({ error: '无效的 Token' });
}
}
3.6 流程关键对比:有状态 vs 无状态
JWT 的核心优势在于其无状态特性。下表对比了传统 Session 认证与 JWT 认证在流程上的关键差异:
| 对比维度 | 传统 Session 认证 | JWT 认证 |
|---|---|---|
| 服务端存储 | 需要存储 Session 数据(内存/Redis) | 无需存储,Token 自包含用户信息 |
| 扩展性 | 水平扩展需共享 Session(如 Redis 集群) | 天然支持水平扩展,任何服务节点均可验证 |
| 跨域支持 | 需配置 CORS 或代理 | 通过 Authorization 头,天然支持跨域 |
| 吊销机制 | 直接删除 Session 即可 | 需配合黑名单或短有效期 + Refresh Token |
3.7 流程中的异常处理
在实际运行中,JWT 流程可能遇到多种异常情况,需要服务端和客户端协同处理:
- Token 过期:客户端应捕获
401状态码,并使用 Refresh Token(如果存在)自动刷新 Access Token,提升用户体验。 - Token 被篡改:签名验证失败时,服务端应记录安全日志,并返回通用错误信息,避免泄露内部细节。
- 权限不足:服务端返回
403 Forbidden,客户端应引导用户联系管理员或切换账号。 - 网络中断:客户端应实现重试机制,但需避免无限重试导致资源耗尽。
最佳实践:将 JWT 的过期时间设置为较短的值(如 15-30 分钟),配合 Refresh Token(有效期较长,如 7 天)使用。这样既保证了安全性(减少 Token 泄露后的风险窗口),又提供了良好的用户体验(自动刷新,无需频繁登录)。