token过期中断后续接口,并静默刷新token后重新调取

这个实现是基于后端支持双token的单点登录模式而论述的。

如此,双token模式下,token 分为长时token短时token。当某个接口触发短时token过期,我们需要借助长时token获取到新的短时token,之后再重新调取之前触发token过期而中断的接口。这个过程用户无感知。

以axios为例,可以实现一个逻辑函数:

let tokenRefreshing = false // 是否在调取刷新token的接口
let retryQueue = [] // 重试队列,以备获取token后重新发起请求
let abortControllers = [] // 撤销队列,以备将请求撤销

function tokenHoldHandle(requestConfig) {
    if (tokenRefreshing) {
        // 正在刷新 token,将请求加入队列
        return new Promise((resolve) => {
            retryQueue.push(requestConfig)
        })
    } else {
        tokenRefreshing = true;
        return refreshToken().then((res) => { // 刷新 session
            setToken(res.sessionId)
            retryQueue.forEach((config) => instance(config)); // 重新发起队列中的请求
            retryQueue = []; // 清空队列
            return instance(requestConfig) // 重新调触发refresh的接口
        }).catch((error) => {
            const errorList = error.response?.data?.errorList || []
            const firstError = errorList[0] || {}

            passiveOut(firstError.errorMessage || 'Token 获取异常,请重新登录!')
        }).finally(() => {
            tokenRefreshing = false
        })
    }
}

在axios全局封装中拦截短token过期状态并调用tokenHoldHandle,拦截长token过期状态跳出到登录页重新登陆:

import axios from 'axios'
import { refreshToken } from '@/apis/login'

// 不依赖token的接口判断,比如调取token的接口、登录接口等
function withoutTokenJudge(requestURL){
    const withoutTokenAPIs = [
        '/auth',
        '/login',
        '/sendVerifyCode',
        '/refreshToken',
    ]
    return withoutTokenAPIs.some(url => {
        return new RegExp(`^${url}`).test(requestURL || '')
    })
}

// 配置全局请求头及通用传参
function setConfig(config){
    const token = Cookies.get(TOKEN_NAME)
    const controller = new AbortController();
    // 不需要 auth token 的接口
    if (withoutTokenJudge(config)) {
        config.timeout = 30 * 1000
        config.headers["sub-sys-kbn"] = "aga"; // 其他头信息
        ......
    } else {
        config.headers.Authorization = `Bearer ${token}`; // 约定的 token 传参
        config.timeout = 60 * 1000

        config.signal = controller.signal
        config._controller = controller; // 可被控制的接口附上控制实例

    }
    return config
}

// 创建axios实例
const instance= axios.create({
    withCredentials: true,
    headers: {
        Accept: 'application/json',
        'Content-Type': 'application/json;charset=UTF-8',
    },
})

// 请求拦截
instance.interceptors.request.use(config => {
    const updatedConfig = setConfig(config)
    if (updatedConfig._controller) {
        // 加入队列,以备撤销请求
        abortControllers.push({
            url: updatedConfig.url,
            method: updatedConfig.method,
            controller: updatedConfig._controller as AbortController
        })
    }
    return updatedConfig
}, (error) => {
    console.log('instance.interceptors.request', error)
    return Promise.reject(error)
})

// 响应拦截
instance.interceptors.response.use(async (response) => {
    // TODO 错误码匹配反馈
    if (response.status === 200) {
        return response.data
    } else {
        console.error('接口返回异常,请重试!')
        return response.data
    }
}, async (error) => {
    const requestConfig = error.config;
    // 错误处理
    if (error.response.status === 401) && !withoutTokenJudge(requestConfig.url!)) {
        if(error.response.code==='shortTokenExpired'){
            // 静默更新短token
            return tokenHoldHandle(requestConfig)
        }else{
            // 中断正在调取的其他接口,并退出到登录页
            abortControllers.forEach((item) => {
                item.controller.abort()
            });
            abortControllers = []
            location.href='/login';
        }
    } else {
        console.error('接口返回异常,请重试!')
    }

    return Promise.reject(error)
})

export default instance;

以上为伪代码,以举例说明为目的,未经测试。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容