前端层面上怎么实现无感token刷新
面试问题前端层面上怎么实现无感token刷新---当时掰扯了很久没有掰扯出来后来面试官说用统一标识收集所有请求这个面试官说的“统一标识收集所有请求”大概率指的是当多个请求同时因为 token 过期返回401时前端只发起一次刷新 token 请求。其他失败请求先挂起等新 token 拿到后再统一重放这些请求。这就是无感刷新 token的核心。问题场景假设页面同时发了 5 个接口请求 A - 401 请求 B - 401 请求 C - 401 请求 D - 401 请求 E - 401如果每个请求都各自去刷新 token就会变成刷新 token 1 次 刷新 token 2 次 刷新 token 3 次 刷新 token 4 次 刷新 token 5 次这样会导致刷新接口并发、token 混乱、请求重复、状态不可控。所以正确做法是第一个 401 请求负责刷新 token 后面的 401 请求进入队列等待 刷新成功后统一携带新 token 重新请求 刷新失败后统一跳登录页核心变量一般会有这几个东西let isRefreshing false let requestQueue []含义是isRefreshing表示当前是否正在刷新 token。requestQueue用来收集刷新期间失败的请求。axios 实现示例import axios from axios const service axios.create({ baseURL: /api, timeout: 10000 }) let isRefreshing false let requestQueue [] function getToken() { return localStorage.getItem(accessToken) } function getRefreshToken() { return localStorage.getItem(refreshToken) } function setToken(token) { localStorage.setItem(accessToken, token) } function setRefreshToken(token) { localStorage.setItem(refreshToken, token) } function refreshToken() { return axios.post(/api/auth/refresh, { refreshToken: getRefreshToken() }) } service.interceptors.request.use(config { const token getToken() if (token) { config.headers.Authorization Bearer ${token} } return config })响应拦截器service.interceptors.response.use( response response, async error { const { response, config } error if (!response) { return Promise.reject(error) } if (response.status ! 401) { return Promise.reject(error) } // 防止同一个请求无限重试 if (config._retry) { return Promise.reject(error) } config._retry true if (isRefreshing) { // 正在刷新 token把当前请求挂起 return new Promise((resolve, reject) { requestQueue.push({ resolve, reject, config }) }) } isRefreshing true try { const refreshRes await refreshToken() const newAccessToken refreshRes.data.accessToken const newRefreshToken refreshRes.data.refreshToken setToken(newAccessToken) if (newRefreshToken) { setRefreshToken(newRefreshToken) } // 刷新成功后重放队列里的请求 requestQueue.forEach(item { item.config.headers.Authorization Bearer ${newAccessToken} item.resolve(service(item.config)) }) requestQueue [] // 重试当前这个触发刷新 token 的请求 config.headers.Authorization Bearer ${newAccessToken} return service(config) } catch (refreshError) { // 刷新失败队列里的请求全部失败 requestQueue.forEach(item { item.reject(refreshError) }) requestQueue [] localStorage.removeItem(accessToken) localStorage.removeItem(refreshToken) // 跳登录页 window.location.href /login return Promise.reject(refreshError) } finally { isRefreshing false } } )这段逻辑怎么跑第一次接口返回401isRefreshing false所以它会发起刷新 token 请求并且把isRefreshing true这期间其他接口也返回401isRefreshing true它们不会再刷新 token而是被放进requestQueue等刷新成功后requestQueue.forEach(item { item.resolve(service(item.config)) })所有挂起的请求都会用新 token 重新发起。面试表达版本你可以这么说无感刷新 token 通常放在 axios 响应拦截器里做。当接口返回 401 时先判断当前是否已经有刷新 token 的请求在执行。如果没有就把它标记为刷新中然后调用 refresh token 接口。如果此时又有其他请求返回 401不让它们重复刷新 token而是把这些请求收集到一个队列里。等 refresh token 成功后更新本地 token再把队列里的请求统一用新 token 重放。如果刷新失败就清空 token队列里的请求统一 reject并跳转登录页。这个基本就是面试官想听的。关键点你可以记住这几个词响应拦截器 401 isRefreshing 标识 请求队列 Promise 挂起 刷新成功后重放请求 刷新失败后统一退出登录 防止无限重试其中面试官说的统一标识就是let isRefreshing false面试官说的收集所有请求就是let requestQueue []还要注意一个坑刷新 token 的接口本身不要再走这套401刷新逻辑否则会死循环。可以这样判断if (config.url /auth/refresh) { return Promise.reject(error) }或者给刷新请求加一个标识axios.post(/api/auth/refresh, data, { skipAuthRefresh: true })然后在拦截器里跳过if (config.skipAuthRefresh) { return Promise.reject(error) }所以你的面试回答可以压缩成一句话前端无感刷新 token 一般是在响应拦截器里处理 401用一个isRefreshing标识保证同一时间只有一个刷新 token 请求其他 401 请求用 Promise 挂起并收集到队列里刷新成功后更新 token 并重放队列请求刷新失败则清空登录态并跳转登录页。