Vue项目,异步队列解决双令牌请求

1,391 阅读7分钟

使用双令牌的初衷

为了方便分布式部署,平台所有接口都允许跨域访问,但为了防止恶意请求,需要设置请求头、令牌等;
有一部分页面不需要登录就可以查看,接口请求带一个静态令牌,有一部分需要有登录权限才能查看,就需要动态令牌。
静态令牌:利用RSA加密与后台约定一个publicKey生成一个加密串,请求接口换取静态令牌;
动态令牌:利用静态令牌请求接口换取动态令牌

踩坑点

1.请求时怎么区分该接口是需要带静态令牌,还是需要带动态令牌?
2.axios的get请求带参数是params:{},post请求是data: {},token怎么携带更优雅?
3.'/coin/users/[uid]/coins/[coin]',接口使用的restful接口,该怎么优雅处理url的参数?
4.token是有过期时间的,静态token、动态token过期该怎么处理?
5.两个token过期后台返回的错误码都是一样的,该怎么处理?
6.如果用户执行一个删除操作,token过期当前请求不能通过,刷新token后怎么自动再执行这个删除请求,否则用户会有点击无效或者卡顿的感受

开始填坑

1.动态token、静态token何时使用?

其实在发送请求的时候只会带一个令牌过去,也就是只会带静态或者只带动态,放在请求的header中发送给后台。  
在用户登录之后返回一个userid并存起来,以此来区分动静令牌,开始的请求都用静态token,如果有userid就用动态token。  
动态令牌的权限高于静态令牌,所有接口都可以用动态token。

2.axios的get请求带参数是params:{},post请求是data: {},token怎么携带更优雅?

如果每次请求都去手动将token带入params或者data很难受,因为这是一个高度重复性的事情;   
索性就放在请求的header中,这样后台获取token也方便,前端也一劳永逸。
import axios from 'axios'
import store from '@/store'

//请求拦截器
axios.interceptors.request.use(config => {
    config.headers['JWT'] = store.getters.JWT;
    config.headers['UID'] = store.getters.uid;

    return config;
}, error => {
    return Promise.reject(error)
})

3.'/coin/users/[uid]/coins/[coin]',接口使用的restful接口,该怎么优雅处理url的参数?

请求传参时,将url上需要的参数一起传入,然后写一个函数来统一处理
//请求拦截器
axios.interceptors.request.use(config => {
    config.headers['JWT'] = store.getters.JWT;
    config.headers['UID'] = store.getters.uid;
    config.url = replaceUrl(config.url, config.method == 'get' ? config.params : config.data)
    return config;
}, error => {
    return Promise.reject(error)
})
/**
 * url特殊变量替换
 * @param {string} url -要请求的url
 * @param {object} params -需要替换进url的值
 */
function replaceUrl(url, params) {
    /**
    * url: '/coin/[articleId]/coins/[coin]'
    * params: {articleId: 198, coin: 'ytx', inviteCount:0, realName: '杰克'}
    * 替换之后 /coin/198/coins/yxt
    **/
    const reg = /\[[a-zA-Z]+\]/g;
    let flag = true;
    let n = 10; //防止无限循环

    while(flag && n > 0) {
        let result = reg.exec(url); //匹配url是否有[***]这种特殊变量
        let item = result ? result[0] : null;
        if(item == '[uid]') { continue; } //如果有[uid]特殊变量,跳过,[uid]会在request的时候处理
        if(item !== null) {
            let key = item.replace('[','').replace(']','');
            let val = params[key]; //有特殊变量,还需要特殊的值去替换
            params[key] = null;
            if(val) {
                url = url.replace(item, val);
            } else {
                console.warn(item + '没有传入对应的值')
            }
        } else {
            flag = false;
        }
        n--;
    }
    return url;
}

4.后面的坑填起来是费时费神,几经周折,最后引入异步队列来解决;所有请求放入数组中排队,上一个请求完成进行下一个请求,如果进行到某一个请求时令牌过期,可以暂停队列等待拿到新的令牌,然后继续执行队列

//创建request.js
import { response } from './response.js'
import axios from 'axios'
import store from '@/store'

const service = axios.create({
    baseURL: process.env.VUE_APP_BASE_URL, // api的base_url  在config中分别设置开发环境和生产环境
    timeout: 20000, // request timeout
    headers: {
        'Content-Type': 'application/json; charset=UTF-8',
    }
});
service.interceptors.request.use(config => {
    config.headers['JWT'] = store.getters.JWT;
    config.headers['Content-Type'] = "application/json; charset=UTF-8";
    config.headers['UID'] = store.getters.uid;

    let uid = Number(store.getters.uid) || null;
    config.url = config.url.replace('[uid]', uid || '');

    // 这几个请求的参数需要在body中传参
    if (config.method == 'post' || config.method == 'put') {
        config.data = config.data || config.params;
    }
    return config;
}, error => {
    alert(error)
    return Promise.reject(error)
})
//返回拦截器
service.interceptors.response.use(res => {
    var respones = res.data;
    return response(res, res.config)
}, error => {
    if (error.toString().indexOf('Network Error') != -1) {
        alert('网络异常,请检查您的网络!')
    }
    return Promise.reject(error)
})


export default service;
// 创建intercept.js,接口请求从intercept走
import store from '../store/index.js'
import { staticToken, superToken, } from './token'
import request from '@/http/request.js'

//队列 
const quee = []; 
//标识队列是否正在进行中,false请求进入队列立即执行,true请求进入队列排队
let wait = false; 

//请求入口
export async function intercept(config) {
    let status = await checkStaticToken()
    if (status) {
        return new Promise(resolve => {
            resolve(add(config))
        })
    }

}

//拓展请求方式
['get','post','put','patch','delete','head','options'].forEach(el => {
    intercept[el] = function(url, data, conf = {}) {
        const options = {
            url,
            method: el,
            ...conf
        }
        if(el == 'post' || el == 'put') {
            options.data = data;
        } else {
            options.params = data;
        }
        options.url = replaceUrl(options.url, data);
        return intercept(options)
    }
})


/**
 * url特殊变量替换
 * @param {string} url -要请求的url
 * @param {object} params -需要替换进url的值
 */
function replaceUrl(url, params) {
    const reg = /\[[a-zA-Z]+\]/g;
    let flag = true;
    let n = 10; //防止无限循环

    while(flag && n > 0) {
        let result = reg.exec(url); //匹配url是否有[***]这种特殊变量
        let item = result ? result[0] : null;
        if(item == '[uid]') { continue; } //如果有[uid]特殊变量,跳过,[uid]会在request的时候处理
        if(item !== null) {
            let key = item.replace('[','').replace(']','');
            let val = params[key]; //有特殊变量,还需要特殊的值去替换
            params[key]=null;
            if(val) {
                url = url.replace(item, val);
            } else {
                console.warn(item + '没有传入对应的值')
            }
        } else {
            flag = false;
        }
        n--;
    }
    return url;
}

//往队列添加
function add(config) {
    return new Promise((resolve, reject) => {
        quee.push({
            resolve,
            config,
            reject,
            count: 0 //防止无限请求
        })
        if(!wait) {
            wait = true;
            run();
        }
    })
    
}

/**
 * 执行队列请求
 * 规则:
 * 所有请求都在队列中排序,run的时候永远只执行队列第一项,
 * 当请求完成(返回错误或返回正确,都算请求完成),让第一项出队列,继续run,这样就能按顺序执行所有请求,
 * 有一个wait参数来标识队列是否正在执行中,如果true就让后面进来的请求排队,false就立即执行当前请求
 */
async function run() {
    let item = quee[0];
    // 如果url上有[uid]标识,但又不存在UID,拒绝请求;或者另外一种方式:跳转登录页
    if(item.config.url.indexOf('[uid]') > -1 && !store.getters.uid) {
        console.warn('url上有[uid]标识,但又不存在UID,拒绝请求');
        item.resolve({code: '500', msg: '没有UID,不发起请求'});
        checkQuee();
        return false;
    }
    //如果重复请求超过三次,防止无限请求,需要停止
    if(item.count > 3) {
        checkQuee();
        return false;
    }
    request(item.config).then(res => {
        if (res && res.code == 'needToken') {
            item.count++;
            //如果有UID,是动态令牌过期,否则就是只需要静态令牌
            if(store.getters.uid) {
                checkSuperToken().then(status => {
                    if(status) {//如果获取动态令牌成功,继续跑队列
                        run()
                    } else {//否则获取到静态->动态令牌之后继续跑队列,如果获取token失败,出队列继续往下走
                        store.dispatch("token", "");
                        getAllToken().then(token => { 
                            token ? run() : checkQuee();
                        })
                    }
                })
            } else {
                store.dispatch("token", "");
                checkStaticToken().then(status => { //获取静态令牌成功继续跑队列,失败什么都不能请求,索性清空队列,
                    if(status) {
                        run()
                    } else {
                        quee.splice(0, quee.length);
                        wait = false;
                        item.resolve({code: '5104',data:'',msg:'获取静态令牌失败'});
                    }
                })
            }
            
            return false;
        }
        checkQuee();
        item.resolve(res)
    }).catch(err => { //如果当前请求失败,则继续执行后面的请求
        quee.splice(0, 1);
        quee.length ? run() : wait = false;
        item.reject(err);
    })
}

//检查队列
function checkQuee() {
    quee.splice(0, 1) //请求成功后将该项移除队列
    quee.length ? run() : wait = false; //如果队列有数据则继续跑,否则通知队列已经执行完毕
}

//请求静态令牌
async function checkStaticToken() {
    if (!store.getters.JWT) {
        let res = await staticToken()
        if (res.code == '0000') {
            store.dispatch("token", res.data);
            return true;
        } else {
            return false;
        }
    }
    return true;
}

//请求动态令牌
async function checkSuperToken() {
    let res = await superToken()
    if (res.code == '0000') {
        store.dispatch("token", res.data);
        return true;
    } else {
        store.dispatch("token", '');
        return false;
    }
}

//依次获取静态->动态令牌
async function getAllToken() {
    let staticT = await staticToken();
    if(staticT.code != '0000') {
        return false;
    }
    store.dispatch("token", staticT.data);

    let dynamic = await superToken();
    if(dynamic.code != '0000') {
        return false;
    }
    store.dispatch("token", dynamic.data);
    return true;
}

6.使用

//main.js中挂载
import { intercept } from './http/intercept'

Vue.prototype.$intercept = intercept
Vue.prototype.$get = intercept.get
Vue.prototype.$post = intercept.post
Vue.prototype.$put = intercept.put
Vue.prototype.$delete = intercept.delete

//在页面中
this.$get('/a/b/c')  //发起get请求
this.$post('/a/b/c',{a,b,c}) //发起post请求

7.其他补充

在请求中不免有些敏感参数,不方便明文传输,可以使用RSA进行加密,点击查看jsencrypt使用方法
在GitHub中查看完整demo github.com/yellowSTA/d…