我们的项目大多涉及很多接口不同类型的请求,每一种请求涉及的参数可能不一样,但他们还是有很多共同
响应拦截,主要处理响应状态码,给一些友好提示,再结合业务,判断这个请求在业务上是成功还是失败。其次就是对 token 过期的处理,如果token 过期了,后端可能返回401状态码,此时我们就需要在请求头上带上refreshToken,自动获取 token 并更新(记得同步更新请求拦截中的 config中的token,否则会死循环),然后再自动发起一次之前的请求;并且处理了多个请求失败引起多次刷新token问题
问题1:用户在使用在线客服座席端的时候,偶尔会出现突然页面跳转到登录页面,需要重新登录
问题2:当refreshToken 过期时候,会停留在首页,无法跳转到登录页 问题3:当存在中断请求时(多个相同请求只发一个请求)
现象原因: 突然跳到登录页面:是由于当前的accessToken 过期了,导致请求失败,在响应拦截器中(axios.interceptor.response.use(config=>{}))中,处理请求失败状态码为401时,可知accessToken 过期,因为跳转到登录页,让用户重新进行登录。 客服聊天可能存在停留时间很长
token无感知刷新
方案:
使用Promise 处理失败的请求,将由于 token 过期导致的失败请求存储起来(存储的是请求回调函数,resolve 状态),后续我们更新了 token ,可以将存储的失败请求重新发起,从而达到用户无感的体验。
@1 创建axios 实例,进行axios 请求前拦截
@2 响应拦截前,封装一个类,定义一个静态变量为 fetchNewTokenPromise 对象
@3 判断响应失败状态码为401的,且 url 地址不是refresh_token的
@4 判断 静态变量 fetchNewTokenPromise 是否为空,为空将赋值为带着 refre_token 获取新的token实例
@4 不为空则等待实例成功直接重新请求,防止多次刷新token
@5 最后成功后置空静态变量 fetchNewTokenPromise
具体思路: @1 登录时获取accessToken 和 refreshToken ,并存储到localStorage中 @2 每次接口请求时,带上accessToken @3 如果 accessToken过期了,接口返回 401 状态码 @4 此时前端会使用 refreshToken 去请求后端再给一个有效的 accessToken @5 重新拿到最新的accessToken 之后,再将刚才请求重新发起 @6 如果refreshToken 也过期了,就只能回到登陆页面重新获取了
实现思路
封装一个 AxiosRetry 类,当 accessToken 过期时,后端返回401,前端拿着 refreshToken 重新获取 accessToken 。
-
类 AxiosRetry 能携带 refreshToken 去重新获取 accessToken
-
当获取到新的 accessToken时,重新发起当前失败的这个请求
注意:config中的data数据格式需要看一下格式,是否满足后端需求
-
注意:当有多个请求并发时,做好拦截,不要多次去获取 accessToken
以上三点需求中,在AxiosRetry类中进行实现:
@1 requestWrapper 函数 会对每个请求进行额外处理,
@2 fetchNewTokenPromise 变量是一个 Promise 实例,只有在请求到新的 accessToken 时,状态才会成功(fulfilled)
实现过程
import Axios from "axios";
// AxiosRetry该类提供一个方法,可以发起请求,带着 refreshToken 去获取新的 accessToken
// 提供一个 requestWrapper 高阶函数,对每一个请求进行额外处理
// 当获取新的 AccessToken 时,可以重新发起刚刚失败了的请求,当有多个请求并发时,要做好拦截,避免多次获取 accessToken
class AxiosRetry {
/**
*Creates an instance of AxiosRetry.
* @param {*} baseUrl 基础url
* @param {*} url 请求新的accessToken的 url
* @param {*} getRefreshToken 获取refreshToken 的函数
* @param {*} unauthorizedCode 无权限的状态码,默认 401
* @param {*} onSuccess 获取新的 accessToken 成功后的回调
* @param {*} onError 获取新的 accessToken 失败后的回调
* @memberof AxiosRetry
*/
constructor(baseUrl, url, getRefreshToken, unauthorizedCode, onSuccess, onError) {
this.baseUrl = baseUrl
this.url = url
this.getRefreshToken = getRefreshToken
this.unauthorizedCode = unauthorizedCode
this.fetchNewTokenPromise = null; // Promise 实例,只有再请求到新的accessToken 时才会fulfilled
this.onSuccess = onSuccess
this.onError = onError
}
// 处理每一个请求的函数
requestWrapper(request) {
return new Promise((resolve,reject) => {
// 先把请求保留下来
const requestFn = request
return request().then(resolve)
.catch(err => {
// 请求新的accessToken的 url,当前失败的地址不是携带refreshToken去获取accessToken地址,要不然死循环?
if (err?.status === this.unauthorizedCode && !(err?.config?.url === this.url)) {
if (!this.fetchNewTokenPromise) {
// 没有获取accessToken请求的实例时
this.fetchNewTokenPromise = this.fetchNewToken()
}
// 有因accessToken 过期导致的其他失败请求,直接等 实例状态成功
this.fetchNewTokenPromise.then(() => {
// 获取成功后,重新执行请求
requestFn().then(resolve).catch(reject)
})
.finally(() => {
// 置空
this.fetchNewTokenPromise = null;
})
} else {
reject()
}
})
})
}
// 获取token 的函数
fetchNewToken() {
return new Axios({
baseURL: this.baseUrl
}).get(this.url, {
headers: {
Authorization: this.getRefreshToken()
}
}).then(this.onSuccess)
.catch(() => {
this.onError();
return Promise.reject();
})
}
}
封装axios请求
import Axios from "axios";
import {AxiosRetry} from './retry.js'
const axios = new Axios({
baseURL: 'http://localhost:8080'
})
// 请求拦截
axios.interceptors.request.use((config) => {
const url = config.url;
if (url !== '/login') {
config.headers.Authorization = localStorage.getItem('LOCAL_ACCESS_KEY')
}
return config
})
// 响应拦截
axios.interceptors.response.use((res) => {
if (res.status !== 200) {
return Promise.reject(res)
}
return JSON.parse(res.data);
})
// 请求逻辑
const axiosRetry = new AxiosRetry({
baseURL: BASE_URL,
url: FETCH_TOKEN_URL,
unauthorizedCode: 401,
getRefreshToken: () => localStorage.getItem(LOCAL_REFRESH_KEY),
onSuccess: res => {
const accessToken = JSON.parse(res.data).accessToken;
localStorage.setItem(LOCAL_ACCESS_KEY,accessToken)
},
onError:()=>console.log('refreshToken 过期了,请重新登录')
})
const get = (url, option) => {
return axiosRetry.requestWrapper(()=> axios.get(url,option))
}
const post = (url, option) => {
return axios.requestWrapper(()=> axios.post(url,option))
}