共计 2858 个字符,预计需要花费 8 分钟才能阅读完成。
问题场景
401 未授权错误是前后端分离架构中的常见问题,通常由以下三种情况触发:

- Token 过期 :JWT 设置的 exp 时间已到
- 无效 Token:签名验证失败或被手动吊销
- 未携带 Token:接口需要认证但请求头缺少 Authorization
传统解决方案是直接跳转登录页,但会带来明显问题:
- 流程中断 :用户正在填写的表单数据会丢失
- 体验割裂 :支付等关键流程被迫中止
- 数据不一致 :前台展示状态与后台实际状态不同步
技术方案对比
方案 A:双 Token 机制
- Access Token:短期有效(如 2 小时),承担常规业务请求
- Refresh Token:长期有效(如 7 天),仅用于获取新 Access Token
优势:
- 即使 Access Token 泄露,攻击窗口期很短
- Refresh Token 可独立设置吊销策略
方案 B:单纯延长 Token 有效期
- 简单粗暴但风险高
- 一旦泄露相当于门户大开
决策建议
选择双 Token 架构,特别是对于:
- 金融类高安全要求应用
- 需要保持长时间登录的 C 端产品
- 跨多服务认证的场景
核心实现
1. Axios 拦截器配置
// 请求拦截器
axios.interceptors.request.use(config => {const token = store.getState().auth.accessToken;
if (token) {config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// 响应拦截器
axios.interceptors.response.use(
response => response,
async error => {
const originalRequest = error.config;
if (error.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
await refreshAccessToken();
return axios(originalRequest);
}
return Promise.reject(error);
}
);
2. Token 刷新队列
let isRefreshing = false;
let failedQueue: any[] = [];
const processQueue = (error: any, token?: string) => {
failedQueue.forEach(prom => {if (error) {prom.reject(error);
} else {prom.resolve(token);
}
});
failedQueue = [];};
async function refreshAccessToken() {if (isRefreshing) {return new Promise((resolve, reject) => {failedQueue.push({ resolve, reject});
});
}
isRefreshing = true;
try {const { data} = await axios.post('/auth/refresh', {refreshToken: getRefreshToken()
});
store.dispatch(updateToken(data.accessToken));
processQueue(null, data.accessToken);
return data.accessToken;
} catch (error) {processQueue(error);
logoutUser();
throw error;
} finally {isRefreshing = false;}
}
3. Redis 存储实践
// Node.js 后端示例
async function refreshTokenHandler(req, res) {const { refreshToken} = req.body;
// 检查 Refresh Token 有效性
const storedToken = await redis.get(`refresh_tokens:${refreshToken}`);
if (!storedToken) {return res.status(401).json({code: 401, msg: '无效的 Refresh Token'});
}
// 生成新 Access Token
const newAccessToken = jwt.sign({ userId: storedToken.userId},
process.env.JWT_SECRET,
{expiresIn: '2h'}
);
// 可选:使旧 Refresh Token 失效
await redis.del(`refresh_tokens:${refreshToken}`);
// 返回新 Token 对
res.json({
accessToken: newAccessToken,
refreshToken: generateNewRefreshToken(storedToken.userId)
});
}
生产环境考量
安全性设计
- Refresh Token 必须设置 httpOnly + Secure Cookie
- 强制 HTTPS 传输
- 每次使用后可选是否轮换 Refresh Token
性能优化
// 令牌接口限流(Express 中间件示例)const rateLimit = require('express-rate-limit');
const refreshLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 分钟
max: 5, // 每个 IP 最多 5 次请求
message: {code: 429, msg: '请求过于频繁'}
});
app.post('/auth/refresh', refreshLimiter, refreshTokenHandler);
监控指标
// 统计 401 发生率
router.use((req, res, next) => {res.on('finish', () => {if (res.statusCode === 401) {statsD.increment('api.unauthorized_requests');
}
});
next();});
避坑指南
- 防止循环刷新
- 记录最近使用的 Refresh Token
-
相同 Token 在短时间内重复请求直接拒绝
-
移动端离线处理
- 使用 Redux Persist 等方案持久化 Token
-
网络恢复后自动重试队列
-
双重过期降级
- 当 Refresh Token 也过期时:
function handleCriticalExpiration() { // 1. 尝试静默登录(如有保存凭证)// 2. 展示非阻塞式重新登录提示 // 3. 保存当前应用状态 }
优化效果
某电商平台实施后数据对比:
| 指标 | 改进前 | 改进后 | 降幅 |
|---|---|---|---|
| 登录页面跳出 | 42% | 15% | 64% |
| 支付中断率 | 8% | 2% | 75% |
开放讨论
在评论区分享你的实践经验:
– 你们项目的 Token 有效期是如何设置的?
– 遇到过哪些意料之外的认证边界情况?
– 如何平衡安全要求与用户体验?
期待看到大家的真知灼见!
正文完
