使用雙令牌的初衷
為了方便分布式部署伴奥,平臺所有接口都允許跨域訪問,但為了防止惡意請求撒强,需要設(shè)置請求頭禽捆、令牌等;
有一部分頁面不需要登錄就可以查看飘哨,接口請求帶一個靜態(tài)令牌胚想,有一部分需要有登錄權(quán)限才能查看,就需要動態(tài)令牌杖玲。
靜態(tài)令牌:利用RSA加密與后臺約定一個publicKey生成一個加密串顿仇,請求接口換取靜態(tài)令牌;
動態(tài)令牌:利用靜態(tài)令牌請求接口換取動態(tài)令牌
踩坑點
1.請求時怎么區(qū)分該接口是需要帶靜態(tài)令牌摆马,還是需要帶動態(tài)令牌臼闻?
2.axios的get請求帶參數(shù)是params:{},post請求是data: {},token怎么攜帶更優(yōu)雅囤采?
3.'/coin/users/[uid]/coins/[coin]'述呐,接口使用的restful接口,該怎么優(yōu)雅處理url的參數(shù)蕉毯?
4.token是有過期時間的乓搬,靜態(tài)token思犁、動態(tài)token過期該怎么處理?
5.兩個token過期后臺返回的錯誤碼都是一樣的进肯,該怎么處理激蹲?
6.如果用戶執(zhí)行一個刪除操作,token過期當前請求不能通過江掩,刷新token后怎么自動再執(zhí)行這個刪除請求学辱,否則用戶會有點擊無效或者卡頓的感受
開始填坑
1.動態(tài)token、靜態(tài)token何時使用环形?
其實在發(fā)送請求的時候只會帶一個令牌過去策泣,也就是只會帶靜態(tài)或者只帶動態(tài),放在請求的header中發(fā)送給后臺抬吟。
在用戶登錄之后返回一個userid并存起來萨咕,以此來區(qū)分動靜令牌,開始的請求都用靜態(tài)token火本,如果有userid就用動態(tài)token危队。
動態(tài)令牌的權(quán)限高于靜態(tài)令牌,所有接口都可以用動態(tài)token发侵。
2.axios的get請求帶參數(shù)是params:{}交掏,post請求是data: {},token怎么攜帶更優(yōu)雅妆偏?
如果每次請求都去手動將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接口膨蛮,該怎么優(yōu)雅處理url的參數(shù)析孽?
請求傳參時,將url上需要的參數(shù)一起傳入,然后寫一個函數(shù)來統(tǒng)一處理
//請求攔截器
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; //防止無限循環(huán)
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.后面的坑填起來是費時費神遭铺,幾經(jīng)周折,最后引入異步隊列來解決振乏;所有請求放入數(shù)組中排隊蔗包,上一個請求完成進行下一個請求,如果進行到某一個請求時令牌過期慧邮,可以暫停隊列等待拿到新的令牌调限,然后繼續(xù)執(zhí)行隊列
//創(chuàng)建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中分別設(shè)置開發(fā)環(huán)境和生產(chǎn)環(huán)境
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 || '');
// 這幾個請求的參數(shù)需要在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('網(wǎng)絡(luò)異常舟陆,請檢查您的網(wǎng)絡(luò)!')
}
return Promise.reject(error)
})
export default service;
// 創(chuàng)建intercept.js耻矮,接口請求從intercept走
import store from '../store/index.js'
import { staticToken, superToken, } from './token'
import request from '@/http/request.js'
//隊列
const quee = [];
//標識隊列是否正在進行中秦躯,false請求進入隊列立即執(zhí)行,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; //防止無限循環(huán)
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();
}
})
}
/**
* 執(zhí)行隊列請求
* 規(guī)則:
* 所有請求都在隊列中排序米母,run的時候永遠只執(zhí)行隊列第一項勾扭,
* 當請求完成(返回錯誤或返回正確,都算請求完成)铁瞒,讓第一項出隊列妙色,繼續(xù)run,這樣就能按順序執(zhí)行所有請求慧耍,
* 有一個wait參數(shù)來標識隊列是否正在執(zhí)行中身辨,如果true就讓后面進來的請求排隊,false就立即執(zhí)行當前請求
*/
async function run() {
let item = quee[0];
// 如果url上有[uid]標識芍碧,但又不存在UID煌珊,拒絕請求;或者另外一種方式:跳轉(zhuǎn)登錄頁
if(item.config.url.indexOf('[uid]') > -1 && !store.getters.uid) {
console.warn('url上有[uid]標識泌豆,但又不存在UID定庵,拒絕請求');
item.resolve({code: '500', msg: '沒有UID,不發(fā)起請求'});
checkQuee();
return false;
}
//如果重復請求超過三次踪危,防止無限請求蔬浙,需要停止
if(item.count > 3) {
checkQuee();
return false;
}
request(item.config).then(res => {
if (res && res.code == 'needToken') {
item.count++;
//如果有UID,是動態(tài)令牌過期贞远,否則就是只需要靜態(tài)令牌
if(store.getters.uid) {
checkSuperToken().then(status => {
if(status) {//如果獲取動態(tài)令牌成功畴博,繼續(xù)跑隊列
run()
} else {//否則獲取到靜態(tài)->動態(tài)令牌之后繼續(xù)跑隊列,如果獲取token失敗,出隊列繼續(xù)往下走
store.dispatch("token", "");
getAllToken().then(token => {
token ? run() : checkQuee();
})
}
})
} else {
store.dispatch("token", "");
checkStaticToken().then(status => { //獲取靜態(tài)令牌成功繼續(xù)跑隊列蓝仲,失敗什么都不能請求俱病,索性清空隊列,
if(status) {
run()
} else {
quee.splice(0, quee.length);
wait = false;
item.resolve({code: '5104',data:'',msg:'獲取靜態(tài)令牌失敗'});
}
})
}
return false;
}
checkQuee();
item.resolve(res)
}).catch(err => { //如果當前請求失敗袱结,則繼續(xù)執(zhí)行后面的請求
quee.splice(0, 1);
quee.length ? run() : wait = false;
item.reject(err);
})
}
//檢查隊列
function checkQuee() {
quee.splice(0, 1) //請求成功后將該項移除隊列
quee.length ? run() : wait = false; //如果隊列有數(shù)據(jù)則繼續(xù)跑亮隙,否則通知隊列已經(jīng)執(zhí)行完畢
}
//請求靜態(tài)令牌
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;
}
//請求動態(tài)令牌
async function checkSuperToken() {
let res = await superToken()
if (res.code == '0000') {
store.dispatch("token", res.data);
return true;
} else {
store.dispatch("token", '');
return false;
}
}
//依次獲取靜態(tài)->動態(tài)令牌
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') //發(fā)起get請求
this.$post('/a/b/c',{a,b,c}) //發(fā)起post請求
7.其他補充
在請求中不免有些敏感參數(shù),不方便明文傳輸擎勘,可以使用RSA進行加密咱揍,點擊查看jsencrypt使用方法;
在GitHub中查看完整demo https://github.com/yellowSTA/doubleToken