如何优雅处理401无token错误:从拦截到自动续期的全链路解决方案

1次阅读
没有评论

共计 2858 个字符,预计需要花费 8 分钟才能阅读完成。

image.webp

问题场景

401 未授权错误是前后端分离架构中的常见问题,通常由以下三种情况触发:

如何优雅处理 401 无 token 错误:从拦截到自动续期的全链路解决方案

  • Token 过期 :JWT 设置的 exp 时间已到
  • 无效 Token:签名验证失败或被手动吊销
  • 未携带 Token:接口需要认证但请求头缺少 Authorization

传统解决方案是直接跳转登录页,但会带来明显问题:

  • 流程中断 :用户正在填写的表单数据会丢失
  • 体验割裂 :支付等关键流程被迫中止
  • 数据不一致 :前台展示状态与后台实际状态不同步

技术方案对比

方案 A:双 Token 机制

  • Access Token:短期有效(如 2 小时),承担常规业务请求
  • Refresh Token:长期有效(如 7 天),仅用于获取新 Access Token

优势:

  • 即使 Access Token 泄露,攻击窗口期很短
  • Refresh Token 可独立设置吊销策略

方案 B:单纯延长 Token 有效期

  • 简单粗暴但风险高
  • 一旦泄露相当于门户大开

决策建议

选择双 Token 架构,特别是对于:

  1. 金融类高安全要求应用
  2. 需要保持长时间登录的 C 端产品
  3. 跨多服务认证的场景

核心实现

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();});

避坑指南

  1. 防止循环刷新
  2. 记录最近使用的 Refresh Token
  3. 相同 Token 在短时间内重复请求直接拒绝

  4. 移动端离线处理

  5. 使用 Redux Persist 等方案持久化 Token
  6. 网络恢复后自动重试队列

  7. 双重过期降级

  8. 当 Refresh Token 也过期时:
    function handleCriticalExpiration() {
      // 1. 尝试静默登录(如有保存凭证)// 2. 展示非阻塞式重新登录提示
      // 3. 保存当前应用状态
    }

优化效果

某电商平台实施后数据对比:

指标 改进前 改进后 降幅
登录页面跳出 42% 15% 64%
支付中断率 8% 2% 75%

开放讨论

在评论区分享你的实践经验:
– 你们项目的 Token 有效期是如何设置的?
– 遇到过哪些意料之外的认证边界情况?
– 如何平衡安全要求与用户体验?

期待看到大家的真知灼见!

正文完
 0
评论(没有评论)