使用双令牌的初衷
为了方便分布式部署,平台所有接口都允许跨域访问,但为了防止恶意请求,需要设置请求头、令牌等;
有一部分页面不需要登录就可以查看,接口请求带一个静态令牌,有一部分需要有登录权限才能查看,就需要动态令牌。
静态令牌:利用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 https://github.com/yellowSTA/doubleToken