实现用户无感知刷新token


1. 需求

前端登录后,后端返回tokenrefreshToken,当token过期时要求用refreshToken去获取新的token,前端需要做到无感知请求刷新token

2. 解决思路

当用户发起一个请求时,判断token是否已过期,若已过期则先调用refreshToken接口,拿到新的token后再继续执行之前的请求。

方法:在响应拦截器中对请求返回结果进行拦截,判断token 返回过期后,调用刷新token接口,然后再重新进行一次请求。

难点:当同时发起多个请求,而刷新token的接口还没返回,此时其他请求该如何处理?

优化

  • 如何防止多次刷新token

​ 如果refreshToken接口还没返回,此时又有一个过期的请求进来,上面的代码就会再一次执行refreshToken,这就会导致多次执行刷新token的接口,因此需要防止这个问题。

我们可以在request.js中用一个flag来标记当前是否正在刷新token的状态,如果正在刷新则不再调用刷新token的接口。

  • 同时发起两个或以上的请求时,其他接口如何重试

​ 两个接口几乎同时发起和返回,第一个接口会进入刷新token后重试的流程,而第二个接口需要先存起来,然后等刷新token后再重试。同样,如果同时发起三个请求,此时需要缓存后两个接口,等刷新token后再重试。由于接口都是异步的,处理起来会有点麻烦。

​ 当第二个过期的请求进来,token正在刷新,我们先将这个请求存到一个数组队列中,想办法让这个请求处于等待中,一直等到刷新token后再逐个重试清空请求队列。

​ 为了解决这个问题,我们得借助Promise。将请求存进队列中后,同时返回一个Promise,让这个Promise一直处于Pending状态(即不调用resolve),此时这个请求就会一直等啊等,只要我们不执行resolve,这个请求就会一直在等待。当刷新请求的接口返回来后,我们再调用resolve,逐个重试。

3. 代码实现

  • src/utils/request.js
// 创建axios实例
const service = axios.create({
    // axios中请求配置有baseURL选项,表示请求URL公共部分
    // baseURL: process.env.VUE_APP_BASE_API,
    baseURL: '/api',
    // 超时
    timeout: 10000
})

// request拦截器
service.interceptors.request.use(config => {
    const token = getToken()
    if (token) {
        config.headers['Authorization'] = 'Bearer ' + token // 让每个请求携带自定义token 请根据实际情况自行修改
    }
    return config;
}, error => {
    console.log(error)
})

// 重试队列,每一项将是一个待执行的函数形式
let requests = []

// 响应拦截器
service.interceptors.response.use(res => {
    if (res.data.status === 10) {
        // 获取当前失败的请求
        const config = res.config;		
        // 判断当前是否正在刷新token
        if (GlobalVariable.refreshFlag === 0) {
            GlobalVariable.refreshFlag = 1;
            // 刷新token
            return refreshToken().then(res => {
                const token = res.data.access_token
                // 在session中记录token信息
                setToken(token)
                setRefreshToken(res.data.refresh_token)
                setScope(res.data.scope)
                // 将token信息传递给后端(如果refreshToken是后端进行操作的,那么token已经保存在后端,就无需传递了)
                setUserToken(res.data)
                // 已经刷新了token,将所有队列中的请求进行重试
                requests.forEach(cb => cb(token))
                // 重试完了清空这个队列
                requests = []
                return service(config)
            }).finally(() => {
                GlobalVariable.refreshFlag = 0;
            })
        } else {
            // 正在刷新token,将返回一个未执行resolve的promise
            return new Promise((resolve) => {
                // 将resolve放进队列,用一个函数形式来保存,等token刷新后直接执行
                requests.push(() => {
                    resolve(service(config))
                })
            })
        }
    }
    console.log(res)
    return res;
}, err => {
    console.log(err);
    return Promise.reject(error)
})

以上已经成功解决无感知刷新token问题。


4. 其他

4.1 前置守卫

记录下前置守卫的写法

  • src/main.js
// 获取url地址栏中的参数
function GetQueryString(name) {
    let reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)");
    let r = window.location.search.substr(1).match(reg);
    if (r != null) return unescape(r[2]);
    return null;
}

// 前置守卫(gard)
// to 将要访问的路径
// from 从哪个路径跳转而来
// next是一个函数 表示放行
// next() 放行 next('/login') 强制跳转到login
// 从from跳转到to
router.beforeEach((to, from, next) => {
    if (getToken()) {
        /* has token*/
        next()  // 直接跳转
    } else {
        // 没有token
        let code = GetQueryString("code");
        if (code) {
            request({
                url: '/oauth/token',
                params: {
                    grant_type: 'authorization_code',
                    code: code,
                    client_id: GlobalVariable.appClientId,
                    client_secret: GlobalVariable.clientSecret,
                    redirect_uri: GlobalVariable.redirectUri,
                },
            }).then((res) => {
                setToken(res.data.access_token)
                setRefreshToken(res.data.refresh_token)
                setScope(res.data.scope)
                setUserToken(res.data).then(() => {
                    getInfo().then(res => {
                        localStorage.setItem("ms_username", res.data.data.login_name)
                        getUserInfo(res.data.data.login_name).then(res => {
                            localStorage.setItem("regionId", res.data.data.regionId)
                            localStorage.setItem("deptId", res.data.data.deptId)
                            localStorage.setItem("deptLevel", res.data.data.deptLevel)
                            window.location.href = GlobalVariable.redirectUri
                        })
                    })
                })
            })
        } else {
            alert("请从用户中心进入!")
            window.location.href = GlobalVariable.loginUrl
        }
    }
    NProgress.done()
})
4.2 token存储方式

存储token有Cookie、 LocalStorage 、 SessionStorage三种方式

三者的异同

特性CookielocalStoragesessionStorage
数据的生命期一般由服务器生成,可设置失效时间。如果在浏览器端生成Cookie,默认是关闭浏览器后失效除非被清除,否则永久保存仅在当前会话下有效,关闭页面或浏览器后被清除
存放数据大小4K左右一般为5MB
与服务器端通信每次都会携带在HTTP头中,如果使用cookie保存过多数据会带来性能问题仅在客户端(即浏览器)中保存,不参与和服务器的通信
易用性需要程序员自己封装,源生的Cookie接口不友好源生接口可以接受,亦可再次封装来对Object和Array有更好的支持

相同点:三者都可以被用来在浏览器端存储数据,而且都是字符串类型的键值对。

参考:

  • 4
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值