4. API 鉴权方案实现: 管理、拦截器自动携带 Authorization、刷新 逻辑
4.1 原理概述
API 鉴权的核心是让服务端能够可靠地识别请求发起方身份。 机制(通常指 JWT)将用户身份信息加密后返回给客户端,客户端在后续请求中通过 Authorization: Bearer <> 头携带该凭证。服务端验证签名后即可恢复用户身份,无需保持会话状态。
鉴权链路包含三个关键环节:获取 (登录时由服务端颁发)、自动携带 (通过客户端拦截器)、 刷新(在 过期前无感获取新凭证)。
4.2 核心实现流程
- 登录获取 :用户提交凭证(用户名/密码),服务端返回 access_(短期有效,如 15 分钟)和 refresh_(长期有效,如 7 天)。
- 本地安全存储:将 access_ 和 refresh_ 存入持久化存储(如 localStorage、SecureStorage 或 Cookie 的 HttpOnly 形式)。
- 请求拦截器自动携带:每次发请求前从存储中读取 access_,注入
Authorization头。 - 响应拦截器检测 401:当后端返回 401 Unauthorized 时,触发刷新流程。
- 无感刷新 :使用 refresh_ 换取新的 access_,重放原请求失败的队列,并更新本地 。
- 并发请求锁:同一时刻多个请求因 过期失败时,只发起一次刷新请求,其他请求排队等待新 。
4.3 管理要点
| 类型 | 生命周期 | 存储位置 | 使用场景 |
|---|---|---|---|
| access_ | 短(15 min ~ 2 h) | 内存 / localStorage | 每次 API 请求的 Authorization 头 |
| refresh_ | 长(7 ~ 30 天) | httpOnly Cookie 或 SecureStorage | 仅用于刷新 access_,不参与业务请求 |
安全提示: 切勿将 refresh_ 暴露给 JavaScript 环境;生产环境应使用 httpOnly + Secure + SameSite=Strict 的 Cookie 传输 refresh_,或使用专门的安全存储 SDK(如 React Native 的 Keychain)。
4.4 内联流程图: 自动携带与刷新机制
4.5 拦截器自动携带 Authorization
以 Axios 为例,在 请求拦截器 中注入 ;在 响应拦截器 中处理 401 并触发刷新。
// 请求拦截器 - 自动携带 access_
axios.interceptors.request.use(
(config) => {
const = localStorage.getItem('access_');
if () {
config.headers.Authorization = `Bearer ${}`;
}
return config;
},
(error) => Promise.reject(error)
);
4.6 刷新 逻辑(含并发锁)
let isRefreshing = false;
let failedQueue = [];
const processQueue = (error, = null) => {
failedQueue.forEach(prom => {
if (error) {
prom.reject(error);
} else {
prom.resolve();
}
});
failedQueue = [];
};
axios.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
if (error.response?.status === 401 && !originalRequest._retry) {
if (isRefreshing) {
// 正在刷新中,将请求加入等待队列
return new Promise((resolve, reject) => {
failedQueue.push({ resolve, reject });
}).then( => {
originalRequest.headers.Authorization = `Bearer ${}`;
return axios(originalRequest);
});
}
originalRequest._retry = true;
isRefreshing = true;
try {
const refreshToken = localStorage.getItem('refresh_');
const { data } = await axios.post('/api/auth/refresh', {
refresh_: refreshToken
});
const newAccessToken = data.access_;
localStorage.setItem('access_', newAccessToken);
// 重放队列中其他请求
processQueue(null, newAccessToken);
// 重放当前请求
originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;
return axios(originalRequest);
} catch (refreshError) {
// 刷新失败,清空 并跳转登录
localStorage.removeItem('access_');
localStorage.removeItem('refresh_');
processQueue(refreshError, null);
window.location.href = '/login';
return Promise.reject(refreshError);
} finally {
isRefreshing = false;
}
}
return Promise.reject(error);
}
);
并发请求优化: 上述代码使用
isRefreshing 标志和 failedQueue 保证了多个 401 并发时只发起一次刷新,避免 被多次刷新导致竞争。实际生产环境建议将队列设计成 Promise 的 resolve/reject 形式,确保所有请求都能按顺序获得新 。4.7 刷新 的 API 约定
// 请求示例
POST /api/auth/refresh
Content-Type: application/json
{
"refresh_": "eyJhbGciOiJIUzI1NiIs..."
}
// 成功响应 (200 OK)
{
"access_": "eyJhbGciOiJSUzI1NiIs...",
"expires_in": 900,
"_type": "Bearer"
}
// 失败响应 (401 / 403)
{
"error": "invalid_grant",
"error_description": "refresh_ expired or revoked"
}
4.8 常见错误及处理策略
| 错误场景 | 现象 | 解决方案 |
|---|---|---|
| Access 未携带 | 接口返回 401,响应头无 Authorization | 检查请求拦截器逻辑;确认 是否从存储中成功读取 |
| Refresh 失效 | 刷新端点返回 401/403,循环刷新失败 | 清除本地 ,强制跳转登录页;使用独立错误码区分 refresh 失败和普通 401 |
| 并发刷新竞争 | 多个请求触发多次刷新, 被覆盖 | 实现队列锁(如上代码);刷新期间禁止其他 401 进入刷新流程 |
| 存储在 localStorage 被 XSS 攻击 | 恶意脚本窃取 | 使用 httpOnly Cookie 传输 refresh_;access_ 存储于内存变量而非持久化存储 |
4.9 性能与稳定性优化
- 预刷新机制: 在 过期前(如剩余 1 分钟)提前发起刷新,减少 401 重试概率。可通过解析 JWT 的
exp字段实现。 - 请求重试请求去重: 对于非 401 错误(如网络超时),同样可用拦截器与队列实现指数退避重试。
- 刷新 超时保护: 为刷新请求设置独立的超时时间(如 5 秒),避免因服务端挂起导致队列无限等待。
- 内存缓存 access_: 首次读取后缓存在模块级变量中,减少 localStorage 读取开销; 更新时同步更新内存。
注意: 预刷新机制会额外增加一次网络请求,不适合高频调用场景;应根据业务 有效期和并发量决定是否启用。在移动端或弱网环境下,建议使用刷新队列配合指数退避重试。