Flutter开发中遇到401错误时如何刷新token并重试请求
401状态下自动刷新Token并重试请求的规范实现方案
核心设计前提
这套逻辑的基础是你的身份鉴权体系采用双Token机制:
- 短有效期的
access_token:用于普通接口请求的身份校验,过期时间通常设置为1-2小时 - 长有效期的
refresh_token:仅用于刷新access_token,过期时间通常设置为7-30天
标准实现步骤
- 请求拦截层统一挂载身份标识
所有业务请求发出前,统一在请求头添加Authorization: Bearer <access_token>,refresh token禁止在普通业务请求中携带,仅可在刷新Token的专用接口中使用。 - 响应拦截层捕获401状态做分层处理
收到响应后优先判断状态码:- 非401状态直接走正常业务响应逻辑
- 触发401时,先校验当前是否存在正在进行的刷新Token请求:
- 无正在刷新的请求:立即标记「刷新中」状态,调用刷新Token接口,拿到新的双Token后更新本地存储,清空「刷新中」标记,依次重新发起所有暂存的待重试请求
- 有正在刷新的请求:将当前请求的所有配置存入待重试队列,返回Pending状态的Promise,等待刷新完成后自动触发重试
- 异常兜底逻辑
若刷新Token接口本身返回401/403,说明refresh_token也已过期,直接清空本地所有身份凭证,跳转至登录页,同时清空待重试队列终止所有重试逻辑。 - 重试次数限制
给每个请求添加最多1次的重试上限,避免逻辑异常导致无限循环请求。
基于Axios的示例代码
import axios from 'axios' const instance = axios.create({ baseURL: '/api', timeout: 10000 }) // 刷新状态标记 let isRefreshing = false // 待重试请求队列 let retryRequests = [] // 请求拦截器 instance.interceptors.request.use( config => { const token = localStorage.getItem('access_token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config }, error => Promise.reject(error) ) // 响应拦截器 instance.interceptors.response.use( response => response.data, async error => { const { config, response } = error // 非401错误直接抛出 if (!response || response.status !== 401) { return Promise.reject(error) } // 刷新接口本身返回401直接跳登录,避免死循环 if (config.url === '/auth/refresh') { localStorage.clear() window.location.href = '/login' return Promise.reject(error) } // 已有重试标记的请求直接抛出,避免无限重试 if (config.__retry) { return Promise.reject(error) } if (!isRefreshing) { isRefreshing = true try { const refreshToken = localStorage.getItem('refresh_token') const res = await axios.post('/api/auth/refresh', { refresh_token: refreshToken }) const { access_token, refresh_token } = res.data // 更新本地存储的Token localStorage.setItem('access_token', access_token) localStorage.setItem('refresh_token', refresh_token) // 执行所有待重试请求 retryRequests.forEach(cb => cb(access_token)) retryRequests = [] // 标记当前请求已重试 config.__retry = true // 重新发起当前请求 return instance(config) } catch (e) { // 刷新失败跳登录 localStorage.clear() window.location.href = '/login' return Promise.reject(e) } finally { isRefreshing = false } } else { // 正在刷新中,将请求加入队列 return new Promise(resolve => { retryRequests.push(token => { config.headers.Authorization = `Bearer ${token}` config.__retry = true resolve(instance(config)) }) }) } } ) export default instance
注意事项
- refresh token安全处理:Web端建议将refresh token存储在HttpOnly、Secure属性的Cookie中,避免XSS攻击窃取,不要存在localStorage/sessionStorage中。
- 幂等性校验:非幂等请求(比如POST提交订单)重试前建议做业务层面的校验,避免重复提交导致业务异常。
- 超时处理:刷新Token接口建议单独设置较短的超时时间,避免长时间无响应用户侧卡死。
内容的提问来源于stack exchange,提问作者Pation
相关产品推荐
相关产品推荐

