前端无感刷新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、一个 refresh_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。

js 复制代码
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 返回(即重新发起请求)。

js 复制代码
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;
})
相关推荐
GISer_Jing2 小时前
前端面试通关:Cesium+Three+React优化+TypeScript实战+ECharts性能方案
前端·react.js·面试
落霞的思绪3 小时前
CSS复习
前端·css
咖啡の猫5 小时前
Shell脚本-for循环应用案例
前端·chrome
百万蹄蹄向前冲7 小时前
Trae分析Phaser.js游戏《洋葱头捡星星》
前端·游戏开发·trae
朝阳5818 小时前
在浏览器端使用 xml2js 遇到的报错及解决方法
前端
GIS之路8 小时前
GeoTools 读取影像元数据
前端
ssshooter9 小时前
VSCode 自带的 TS 版本可能跟项目TS 版本不一样
前端·面试·typescript
你的人类朋友9 小时前
【Node.js】什么是Node.js
javascript·后端·node.js
Jerry9 小时前
Jetpack Compose 中的状态
前端
dae bal10 小时前
关于RSA和AES加密
前端·vue.js