4. 令牌过期处理:服务端验证 exp、过期后返回 401 状态码、客户端刷新令牌流程
JWT 令牌的 exp(过期时间)声明是后端安全合规的第一道防线。本节将深入讲解服务端如何严格验证 exp、过期后如何返回标准 401 状态码,以及客户端如何通过刷新令牌(Refresh Token)机制实现无感续期。
4.1 服务端 exp 验证机制
服务端在每次收到 JWT 访问令牌时,必须执行以下验证步骤:
- 解码令牌:解析 JWT 的 Header 和 Payload。
- 验证签名:使用服务端密钥或公钥验证令牌未被篡改。
- 检查 exp 声明:将当前时间戳与
exp字段对比。 - 判断结果:若当前时间 >=
exp,则判定令牌过期。
关键合规点: 服务端必须使用 UTC 时间进行比较,避免时区差异导致验证漏洞。推荐使用
System.currentTimeMillis() / 1000(Java)或 datetime.utcnow().timestamp()(Python)获取标准时间戳。
4.2 过期后返回 401 状态码
当服务端检测到令牌过期时,必须返回 HTTP 401 Unauthorized 状态码,并在响应体中携带标准错误信息,便于客户端识别并触发刷新流程。
| 字段 | 值 | 说明 |
|---|---|---|
| HTTP 状态码 | 401 | 标准未授权状态码 |
| WWW-Authenticate | Bearer error="invalid_token", error_description="The access token expired" | RFC 6750 标准错误头 |
| 响应体 code | TOKEN_EXPIRED | 业务错误码,用于客户端逻辑判断 |
| 响应体 message | 访问令牌已过期,请刷新 | 用户友好的提示信息 |
// 服务端验证 exp 并返回 401 示例(Node.js Express 中间件)
const jwt = require('jsonwebtoken');
function authMiddleware(req, res, next) {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
return res.status(401).json({
code: 'TOKEN_MISSING',
message: '缺少访问令牌'
});
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
next();
} catch (err) {
if (err.name === 'TokenExpiredError') {
return res.status(401).json({
code: 'TOKEN_EXPIRED',
message: '访问令牌已过期,请刷新'
});
}
return res.status(401).json({
code: 'TOKEN_INVALID',
message: '无效的访问令牌'
});
}
}
4.3 客户端刷新令牌流程
客户端收到 401 且错误码为 TOKEN_EXPIRED 时,应自动触发刷新令牌流程,避免用户频繁重新登录。
最佳实践: 刷新令牌(Refresh Token)应具有更长的有效期(如 7 天),且必须存储在安全位置(HttpOnly Cookie 或 Secure Storage),避免 XSS 攻击窃取。
4.4 令牌过期与刷新流程 SVG 图
4.5 刷新令牌安全对比
| 对比维度 | Access Token | Refresh Token |
|---|---|---|
| 有效期 | 短(15 分钟 ~ 1 小时) | 长(7 天 ~ 30 天) |
| 存储位置 | 内存 / 短期缓存 | HttpOnly Cookie / Secure Storage |
| 传输频率 | 每次 API 请求 | 仅在过期刷新时 |
| 泄露风险 | 高(频繁传输) | 低(极少传输) |
| 吊销机制 | 依赖短有效期自动失效 | 服务端黑名单 / 版本号控制 |
安全警告: 切勿将 Refresh Token 存储在 localStorage 或 sessionStorage 中,这些存储方式易受 XSS 攻击。推荐使用 HttpOnly + Secure + SameSite=Strict 的 Cookie 存储 Refresh Token。
4.6 客户端刷新令牌代码示例
// 客户端 Axios 拦截器实现自动刷新
import axios from 'axios';
const apiClient = axios.create({
baseURL: 'https://api.example.com',
withCredentials: true // 携带 HttpOnly Cookie 中的 Refresh Token
});
let isRefreshing = false;
let failedQueue = [];
const processQueue = (error, token = null) => {
failedQueue.forEach(prom => {
if (error) {
prom.reject(error);
} else {
prom.resolve(token);
}
});
failedQueue = [];
};
apiClient.interceptors.response.use(
response => response,
async error => {
const originalRequest = error.config;
if (error.response?.status === 401 &&
error.response?.data?.code === 'TOKEN_EXPIRED' &&
!originalRequest._retry) {
if (isRefreshing) {
return new Promise((resolve, reject) => {
failedQueue.push({ resolve, reject });
}).then(token => {
originalRequest.headers.Authorization = `Bearer ${token}`;
return apiClient(originalRequest);
});
}
originalRequest._retry = true;
isRefreshing = true;
try {
const response = await axios.post(
'https://auth.example.com/refresh',
{},
{ withCredentials: true }
);
const newToken = response.data.accessToken;
processQueue(null, newToken);
originalRequest.headers.Authorization = `Bearer ${newToken}`;
return apiClient(originalRequest);
} catch (refreshError) {
processQueue(refreshError, null);
// 刷新失败,跳转登录页
window.location.href = '/login';
return Promise.reject(refreshError);
} finally {
isRefreshing = false;
}
}
return Promise.reject(error);
}
);
4.7 令牌过期处理与刷新流程(简化版)
4.8 常见问题与避坑指南
- 时钟偏差问题: 服务端和客户端时间不同步可能导致令牌提前过期或延迟过期。建议服务端设置
exp时预留 30 秒的时钟偏差容忍窗口。 - 并发刷新竞态: 多个请求同时触发刷新时,应使用队列机制(如上述代码中的
failedQueue)避免重复刷新。 - Refresh Token 轮换: 每次使用 Refresh Token 后,应颁发新的 Refresh Token 并废弃旧的,防止重放攻击。
- 401 状态码误判: 并非所有 401 都是令牌过期,需通过错误码
TOKEN_EXPIRED区分,避免对无效令牌也执行刷新操作。