axios中实现无感刷新token

2021-11-24 13:48:23 浏览数 (3)

现状

项目采用前后端分离开发,前后端使用access_token(即token)进行交互认证,但access_token有一个有效期,在access_token过期后,请求接口将无法成功,现在的处理方式是直接退出跳转至登录入口要求重新登录,但这种方式体验非常不友好,如果当前用户正在录入大量数据时token已经失效,提交数据时直接就退出了,从产品及交互上这种方式是不允许的。

分析

后端采用 IdentityService4

构建认证与授权,在登录成功后除返回access_token之外,增加了expires_in、refresh_token。并增加交换token接口。

expires_in :access_token的过期时间。

refresh_token

:刷新token。access_token过期后可以使用refresh_token交换新的access_token。一个refresh_token只能使用一次。

交换token接口

:使用refresh_token交换access_token,得到新的access_token、新的expires_in、新的refresh_token。

那么前端刷新token即可有两种方式

1、在request请求之前进行拦截,根据expires_in计算出当前token是否过期,若已过期,则将请求挂起,先调用交换token接口,得到新的access_token后再继续请求。

这里本项目放弃此方式。

2、后端接口在检查到access_token过期后,返回状态码40001(前后端约定值),那么在response中进行拦截,当返回状态码为40001时,调用交换token接口,得到新的access_token后再将原请求重发。

实现

对axios进行封装

代码语言:txt复制
import axios from 'axios';
代码语言:txt复制
import router from '@/router'
代码语言:txt复制
import Vue from 'vue'
代码语言:txt复制
import {  Loading } from 'element-ui';
代码语言:txt复制
import  qs from  'qs';
代码语言:txt复制
let host = window.g.ApiUrl
代码语言:txt复制
let loadingInstance; //loading 实例
代码语言:txt复制
let needLoadingRequestCount = 0; //当前正在请求的数量
代码语言:txt复制
//是否有请求正在刷新token
代码语言:txt复制
let isRefreshing = false
代码语言:txt复制
// 重试请求队列 每一项都是一个待执行待函数
代码语言:txt复制
let requests= [];
代码语言:txt复制
//Loading 封装
代码语言:txt复制
/*
代码语言:txt复制
* 打开全页loading
* this.$showLoading()
* */
Vue.prototype.$showLoading = function(text='加载中...'){
    if (needLoadingRequestCount == 0) {
        loadingInstance = Loading.service({text: text});
    }
    needLoadingRequestCount  ;
};
/*
* 关闭全页loading
* this.$closeLoading()
* */
Vue.prototype.$closeLoading = function(type=0){
    needLoadingRequestCount--;
    if(type == 1){
        loadingInstance.close();
        return false;
    }
    if (needLoadingRequestCount <= 0) {
        loadingInstance.close();
    }
}
/**
 * 刷新token
 */
function refreshToken (response,instance) {
    const refreshtoken = sessionStorage.getItem('refresh_token');
    // 判断 没有refresh_token的处理
    if (!refreshtoken) {
        sessionStorage.removeItem('access_token')
        sessionStorage.removeItem('sso_token')
        sessionStorage.removeItem('expires_in')
        sessionStorage.removeItem('refresh_token')
        window.location.href = window.g.mainSiteUrl;//返回登陆
    }
    let param = {
        client_id: window.g.client_id,
        client_secret: window.g.client_secret,
        grant_type: 'refresh_token',
        refresh_token: refreshtoken
    };
    // instance是当前已创建的axios实例
    return instance.post('/connect/token',qs.stringify(param)).then(res => {
        //业务系统token
        sessionStorage.setItem('access_token', res.access_token);
        //业务系统token过期时间
        sessionStorage.setItem('expires_in',res.expires_in);
        //业务系统refresh_token
        sessionStorage.setItem('refresh_token', res.refresh_token);
代码语言:txt复制
        // 重新请求接口 前过期的接口
代码语言:txt复制
        response.config.headers.Authorization ="Bearer "  sessionStorage.getItem('access_token');
代码语言:txt复制
        // 已经刷新了token,将所有队列中的请求进行重试,最后再清空队列
代码语言:txt复制
        requests.forEach(cb => cb( res.access_token))
代码语言:txt复制
        requests = []
代码语言:txt复制
        return instance(response.config)
代码语言:txt复制
    }).catch(res => {
代码语言:txt复制
        sessionStorage.removeItem('access_token')
代码语言:txt复制
        sessionStorage.removeItem('sso_token')
代码语言:txt复制
        sessionStorage.removeItem('expires_in')
代码语言:txt复制
        sessionStorage.removeItem('refresh_token')
代码语言:txt复制
        //返回登陆
代码语言:txt复制
        window.location.href = window.g.mainSiteUrl;
代码语言:txt复制
    }).finally(() => {
代码语言:txt复制
        isRefreshing = false
代码语言:txt复制
    })
代码语言:txt复制
}
代码语言:txt复制
export default function $axios(options) {
代码语言:txt复制
    return new Promise((resolve, reject) => {
代码语言:txt复制
        const instance = axios.create({
代码语言:txt复制
            baseURL: host,
代码语言:txt复制
            isEditContentType:true,
代码语言:txt复制
            isUpload:false
代码语言:txt复制
        })
代码语言:txt复制
        // request 拦截器
代码语言:txt复制
        instance.interceptors.request.use(
代码语言:txt复制
            config => {
代码语言:txt复制
                Vue.prototype.$showLoading();
代码语言:txt复制
                if(config.url!='/connect/token' && config.isEditContentType){
代码语言:txt复制
                    config.headers["Content-Type"]='application/json;charset=UTF-8'
代码语言:txt复制
                }else if(config.url==='/connect/token'){
代码语言:txt复制
                    config.headers["Content-Type"]='application/x-www-form-urlencoded'
代码语言:txt复制
                }
代码语言:txt复制
                let token = sessionStorage.getItem('access_token')
代码语言:txt复制
                if (token && !config.isUpload) {
代码语言:txt复制
                    config.headers.Authorization = "Bearer " token
代码语言:txt复制
                } 
代码语言:txt复制
                // 根据请求方法,序列化传来的参数,根据后端需求是否序列化
代码语言:txt复制
                if (config.method === 'post') {
代码语言:txt复制
                    // if (config.data.__proto__ === FormData.prototype
代码语言:txt复制
                    //   || config.url.endsWith('path')
代码语言:txt复制
                    //   || config.url.endsWith('mark')
代码语言:txt复制
                    //   || config.url.endsWith('patchs')
代码语言:txt复制
                    // ) {
代码语言:txt复制
                    // } else {
代码语言:txt复制
                    //config.data = JSON.stringify(config.data)
代码语言:txt复制
                    // }
代码语言:txt复制
                }else if (config.method === 'get') { //get请求增加时间戳
代码语言:txt复制
                    let url = config.url;
代码语言:txt复制
                    url.indexOf('?') === -1 ? config.url = url '?_=' (new Date().getTime()) : config.url = url '&_=' (new Date().getTime());
代码语言:txt复制
                }
代码语言:txt复制
                return config
代码语言:txt复制
            },
代码语言:txt复制
            error => {
代码语言:txt复制
                Vue.prototype.$closeLoading();
代码语言:txt复制
                // 请求错误时
代码语言:txt复制
                // 1. 判断请求超时
代码语言:txt复制
                if (error.code === 'ECONNABORTED' && error.message.indexOf('timeout') !== -1) {
代码语言:txt复制
                    // return service.request(originalRequest);// 再重复请求一次
代码语言:txt复制
                }
代码语言:txt复制
                // 2. 需要重定向到错误页面
代码语言:txt复制
                const errorInfo = error.response
代码语言:txt复制
                if (errorInfo) {
代码语言:txt复制
                    error = errorInfo.data  // 页面那边catch的时候就能拿到详细的错误信息,看最下边的Promise.reject
代码语言:txt复制
                    const errorStatus = errorInfo.status; // 404 403 500 ...
代码语言:txt复制
                    router.push({
代码语言:txt复制
                        path: `/error/${errorStatus}`
代码语言:txt复制
                    })
代码语言:txt复制
                }
代码语言:txt复制
                return Promise.reject(error) // 在调用的那边可以拿到(catch)你想返回的错误信息
代码语言:txt复制
            }
代码语言:txt复制
        )
代码语言:txt复制
        // response 拦截器
代码语言:txt复制
        instance.interceptors.response.use(response => {
代码语言:txt复制
            Vue.prototype.$closeLoading();
代码语言:txt复制
            const code  = response.data.code
代码语言:txt复制
            //接口返回token超时
代码语言:txt复制
            if (code === "40001") {
代码语言:txt复制
                var config = response.config;
代码语言:txt复制
                //当前是否有已经在刷新token,防止多次请求刷新token
代码语言:txt复制
                if (!isRefreshing) {
代码语言:txt复制
                    //没有则请求刷新token
代码语言:txt复制
                    isRefreshing = true
代码语言:txt复制
                    return refreshToken(response,instance)
代码语言:txt复制
                } else {
代码语言:txt复制
                    // 正在刷新token,加入队列中,将返回一个未执行resolve的promise
代码语言:txt复制
                    return new Promise((resolve) => {
代码语言:txt复制
                        // 将resolve放进队列,用一个函数形式来保存,等token刷新后直接执行
代码语言:txt复制
                        requests.push((token) => {
代码语言:txt复制
                            config.headers.Authorization ="Bearer "  token;
代码语言:txt复制
                            resolve(instance(config))
代码语言:txt复制
                        })
代码语言:txt复制
                    })
代码语言:txt复制
                }
代码语言:txt复制
            }
代码语言:txt复制
            let data;
代码语言:txt复制
            if (response.data == undefined) {
代码语言:txt复制
                data = JSON.parse(response.request.responseText)
代码语言:txt复制
            } else {
代码语言:txt复制
                data = response.data
代码语言:txt复制
            }
代码语言:txt复制
            return data
代码语言:txt复制
        }, error => {
代码语言:txt复制
            return Promise.reject(error)
代码语言:txt复制
        })
代码语言:txt复制
        // 请求处理
代码语言:txt复制
        instance(options).then(res => {
代码语言:txt复制
            resolve(res)
代码语言:txt复制
            return false
代码语言:txt复制
        }).catch(error => {
代码语言:txt复制
            reject(error)
代码语言:txt复制
        })
代码语言:txt复制
    })
代码语言:txt复制
}

0 人点赞