前端无感刷新token机制(一文说明白)
前言
用户登录之后,会返回一个用户的标识,之后带上这个标识请求别的接口,就能识别出该用户。
标识登录状态的方案有两种: session
和 jwt
。这两种方案一个服务端存储,通过 cookie
携带标识,一个在客户端存储,通过 header 携带标识。
session
是通过 cookie
返回一个 id
,关联服务端内存里保存的 session
对象,请求时服务端取出 cookie
里 id
对应的 session
对象,就可以拿到用户信息。
jwt
不在服务端存储,会直接把用户信息放到 token
里返回,每次请求带上这个 token
,服务端就能从中取出用户信息。
session
的方案默认不支持分布式,因为是保存在一台服务器的内存的,另一台服务器没有。jwt
的方案天然支持分布式,因为信息保存在 token
里,只要从中取出来就行。
为什么需要无感刷新token机制
服务端把用户信息放入 token
里,设置一个过期时间,客户端请求的时候通过 authorization
的 header
携带 token
,服务端验证通过,就可以从中取到用户信息。
但是token
是有过期时间的,比如 3 天,那过期后再访问就需要重新登录了。这样体验并不好。 想想你在用某个 app 的时候,用着用着突然跳到登录页了,告诉你需要重新登录了。是不是体验很差?
所以要加上续签机制,也就是延长 token
过期时间。
主流的方案是通过双 token
,一个 access_token
、一个 refresh_token
(一个短token
,一个长token
)。
无感刷新token机制
用户登录成功之后,两个 token
(一个 access_token
、一个 efresh_token
),访问接口时携带 access_token
访问,当 access_token
过期时,通过 refresh_token
来刷新,拿到新的 access_token
和 refresh_token
。
而 access_token
一般过期时间设置的比较短,比如 30 分钟,refresh_token
设置的过期时间比较长,比如 7 天。这样,只要你 7 天内访问一次,就能刷新 token,再续 7 天,一直不需要登录。
但如果你超过 7 天没访问,那 refresh_token
也过期了,就需要重新登录了。想想你常用的 APP,是不是没再重新登录过?而不常用的 APP,再次打开是不是就又要重新登录了?这种一般都是双 token
做的。
实现
在axios的响应拦截器中刷新token
。
这里还需要排除下 /refresh
接口,也就是刷新失败不继续刷新,不然会进入死循环。
刷新 token
成功,就重发之前的请求,否则,提示重新登录。其他错误直接返回。
在刷新 token
的接口里,拿到新的 access_token
和 refresh_token
后,更新本地存储的 token
。
axiosInstance.interceptors.response.use(
(response) => {
return response;
},
async (error) => {
let { data, config } = error.response;
if (data.statusCode === 401 && !config.url.includes('/refresh')) {
const res = await refreshToken();
if(res.status === 200) {
return axiosInstance(config);
} else {
alert(data || '登录过期,请重新登录');
}
} else {
return error.response;
}
}
)
async function refreshToken() {
const res = await axiosInstance.get('/refresh', {
params: {
token: localStorage.getItem('refresh_token')
}
});
localStorage.setItem('access_token', res.data.accessToken);
localStorage.setItem('refresh_token', res.data.refreshToken);
return res;
}
但是还有一些问题,如果并发请求,多次调用后端接口,会刷新token多次,解决方案如下:
加一个 refreshing
的标记,记录是否正在刷新token
,如果在刷新,那就返回一个 promise
,并且把它的 resolve
方法还有 config
加入到一个队列里。
当 token
刷新 成功之后,重新发送队列中的请求(即在刷新期间积压的请求),并且把结果通过 resolve
返回(即重新发起请求)。
interface PendingTask {
config: AxiosRequestConfig
resolve: Function
}
let refreshing = false;
const queue: PendingTask[] = [];
axiosInstance.interceptors.response.use(
(response) => {
return response;
},
async (error) => {
let { data, config } = error.response;
if(refreshing) {
return new Promise((resolve) => {
queue.push({
config,
resolve
});
});
}
if (data.statusCode === 401 && !config.url.includes('/refresh')) {
refreshing = true;
const res = await refreshToken();
refreshing = false;
if(res.status === 200) {
queue.forEach(({config, resolve}) => {
resolve(axiosInstance(config))
})
return axiosInstance(config);
} else {
alert(data || '登录过期,请重新登录');
}
} else {
return error.response;
}
}
)
axiosInstance.interceptors.request.use(function (config) {
const accessToken = localStorage.getItem('access_token');
if(accessToken) {
config.headers.authorization = 'Bearer ' + accessToken;
}
return config;
})