axios中实现无感刷新token

现状

项目采用前后端分离开发,前后端使用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进行封装

import axios from 'axios';
import router from '@/router'
import Vue from 'vue'
import {  Loading } from 'element-ui';
import  qs from  'qs';

let host = window.g.ApiUrl

let loadingInstance; //loading 实例
let needLoadingRequestCount = 0; //当前正在请求的数量
//是否有请求正在刷新token
let isRefreshing = false
// 重试请求队列 每一项都是一个待执行待函数
let requests= [];

//Loading 封装
/*
* 打开全页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);

        // 重新请求接口 前过期的接口
        response.config.headers.Authorization ="Bearer "+ sessionStorage.getItem('access_token');
        // 已经刷新了token,将所有队列中的请求进行重试,最后再清空队列
        requests.forEach(cb => cb( res.access_token))
        requests = []
        return instance(response.config)
    }).catch(res => {
        sessionStorage.removeItem('access_token')
        sessionStorage.removeItem('sso_token')
        sessionStorage.removeItem('expires_in')
        sessionStorage.removeItem('refresh_token')
        //返回登陆
        window.location.href = window.g.mainSiteUrl;
    }).finally(() => {
        isRefreshing = false
    })
}

export default function $axios(options) {
    return new Promise((resolve, reject) => {
        const instance = axios.create({
            baseURL: host,
            isEditContentType:true,
            isUpload:false
        })

        // request 拦截器
        instance.interceptors.request.use(
            config => {
                Vue.prototype.$showLoading();
                if(config.url!='/connect/token' && config.isEditContentType){
                    config.headers["Content-Type"]='application/json;charset=UTF-8'
                }else if(config.url==='/connect/token'){
                    config.headers["Content-Type"]='application/x-www-form-urlencoded'
                }
                let token = sessionStorage.getItem('access_token')
                if (token && !config.isUpload) {
                    config.headers.Authorization = "Bearer "+token
                } 
                // 根据请求方法,序列化传来的参数,根据后端需求是否序列化
                if (config.method === 'post') {
                    // if (config.data.__proto__ === FormData.prototype
                    //   || config.url.endsWith('path')
                    //   || config.url.endsWith('mark')
                    //   || config.url.endsWith('patchs')
                    // ) {

                    // } else {
                    //config.data = JSON.stringify(config.data)
                    // }
                }else if (config.method === 'get') { //get请求增加时间戳
                    let url = config.url;
                    url.indexOf('?') === -1 ? config.url = url+'?_='+(new Date().getTime()) : config.url = url+'&_='+(new Date().getTime());
                }
                return config
            },

            error => {
                Vue.prototype.$closeLoading();
                // 请求错误时
                // 1. 判断请求超时
                if (error.code === 'ECONNABORTED' && error.message.indexOf('timeout') !== -1) {
                    // return service.request(originalRequest);// 再重复请求一次
                }
                // 2. 需要重定向到错误页面
                const errorInfo = error.response
                if (errorInfo) {
                    error = errorInfo.data  // 页面那边catch的时候就能拿到详细的错误信息,看最下边的Promise.reject
                    const errorStatus = errorInfo.status; // 404 403 500 ...
                    router.push({
                        path: `/error/${errorStatus}`
                    })
                }
                return Promise.reject(error) // 在调用的那边可以拿到(catch)你想返回的错误信息
            }
        )

        // response 拦截器
        instance.interceptors.response.use(response => {
            Vue.prototype.$closeLoading();
            const code  = response.data.code
            //接口返回token超时
            if (code === "40001") {
                var config = response.config;
                //当前是否有已经在刷新token,防止多次请求刷新token
                if (!isRefreshing) {
                    //没有则请求刷新token
                    isRefreshing = true
                    return refreshToken(response,instance)
                } else {
                    // 正在刷新token,加入队列中,将返回一个未执行resolve的promise
                    return new Promise((resolve) => {
                        // 将resolve放进队列,用一个函数形式来保存,等token刷新后直接执行
                        requests.push((token) => {
                            config.headers.Authorization ="Bearer "+ token;
                            resolve(instance(config))
                        })
                    })
                }
            }
            let data;
            if (response.data == undefined) {
                data = JSON.parse(response.request.responseText)
            } else {
                data = response.data
            }
            return data
        }, error => {
            return Promise.reject(error)
        })

        // 请求处理
        instance(options).then(res => {
            resolve(res)
            return false
        }).catch(error => {
            reject(error)
        })
    })
}

注意:
1、在所有需要清除access_token的地方都同时需要将refresh_token一并清除,否则可能将通过refresh_token获取到新的token。
2、从安全考虑,access_token设置有效期时间较短。refresh_token有效期可设置为用户未操作而需要退出的时间。这样可以实现:当用户一直在线的情况下,token不会突然无故失效而退出。同时如果用户间隔一段时间未操作(大于refresh_token的有效期,因为refresh_token过期后获取新access_token将不会成功),那么再次操作将退出让其重新登录。

在项目实现过程中参考了以下博主的文章,博主写的非常详细。
https://segmentfault.com/a/1190000020210980?_ea=153309043

本文仅供自己开发过程留作笔记。

你可能感兴趣的:(axios中实现无感刷新token)