最近學(xué)習(xí)vue-cli3搭建后臺管理項目,關(guān)于系統(tǒng)登錄攔截和獲取用戶權(quán)限控制這一塊是卡了挺久的一個難點,后臺項目權(quán)限驗證與安全性是非常重要的,可以說是一個后臺項目一開始就必須考慮和搭建的基礎(chǔ)核心功能。這篇文章寫一下前后端分離下的登錄解決方案,目前大多數(shù)都采用請求頭攜帶 Token 的形式。
一、整體思路
- 首次登錄時,后端服務(wù)器判斷用戶賬號密碼正確之后,根據(jù)用戶id、用戶名、定義好的秘鑰、過期時間生成 token ,返回給前端;
- 前端拿到后端返回的 token ,存儲在 localStroage 和 Vuex 里;
- 前端每次路由跳轉(zhuǎn),判斷 localStroage 有無 token ,沒有則跳轉(zhuǎn)到登錄頁,有則請求獲取用戶信息,改變登錄狀態(tài);
- 每次請求接口,在 Axios 請求頭里攜帶 token;
- 后端接口判斷請求頭有無 token,沒有或者 token 過期,返回401;
- 前端得到 401 狀態(tài)碼,重定向到登錄頁面,回到第一步。
二、封裝axios
登錄成功后,把后臺返回的 Token 存在localStroage,檢查有無 Token ,每次請求在 Axios 請求頭上進(jìn)行攜帶
如果狀態(tài)碼是401,則有可能是 Token 過期,需要跳轉(zhuǎn)到登錄頁進(jìn)行登錄重新獲取Token。
axios封裝需根據(jù)后臺返回的數(shù)據(jù)格式封裝,例如我司后臺返回數(shù)據(jù)格式如下:

import axios from "axios";
import { Message } from "iview";
let router = import("@/router");
axios.defaults.baseURL = "/api";
axios.defaults.headers.post["Content-Type"] = "application/json;charset=UTF-8";
axios.defaults.headers["X-Requested-With"] = "XMLHttpRequest";
axios.defaults.headers["Cache-Control"] = "no-cache";
axios.defaults.headers["pragma"] = "no-cache";
let source = axios.CancelToken.source();
//請求添加token
axios.interceptors.request.use(request => {
request.headers["Authorization"] = window.localStorage.getItem('token') ? window.localStorage.getItem('token') : "";
return request;
});
//登錄過期(token失效)跳轉(zhuǎn)到登錄頁
axios.interceptors.response.use(response => {
let data = response.data;
if (
data.state && [401].includes(data.state.code)
) {
router.then(lib => {
if (lib.default.currentRoute.name === "login") return;
lib.default.push({
name: "login"
})
Message.warning(data.state.msg);
});
}
return response;
})
//返回值解構(gòu)
axios.interceptors.response.use(response => {
let data = response.data;
let isJson = (response.headers["content-type"] || "").includes("json");
if (isJson) {
if (data.state && data.state.code === 200) {
return Promise.resolve({
data: data.data,
msg: data.state.msg,
code: data.state.code,
page: data.page
});
}
return Promise.reject(
data.state &&
data.state.msg ||
"網(wǎng)絡(luò)錯誤"
);
} else {
return data;
}
}, err => {
let isCancel = axios.isCancel(err);
if (isCancel) {
return new Promise(() => {});
}
return Promise.reject(
err.response.data &&
err.response.data.state &&
err.response.data.state.msg ||
"網(wǎng)絡(luò)錯誤"
);
})
//切換頁面取消請求
axios.interceptors.request.use(request => {
request.cancelToken = source.token;
return request;
});
router.then(lib => {
lib.default.beforeEach((to, from, next) => {
source.cancel()
source = axios.CancelToken.source();
next()
})
})
export function post(url, data, otherConfig) {
return axios.post(url, data, otherConfig);
}
export function get(url, data, otherConfig) {
return axios.get(url, {
params: data,
...otherConfig
});
}
三、路由攔截
首先在定義路由的時候就需要多添加一個自定義字段requireAuth,用于判斷用戶訪問該路由時是否需要登錄。例如:在用戶直接跳轉(zhuǎn)mainPage頁面的時候,需要判斷用戶是否登錄,那么我們在該路由下添加meta字段。
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes: [{
path: '/',
name: 'MainPage',
component: mainPage,
children: pages,
meta: {
requiresAuth: true // 訪問該路由時需要判斷是否登錄
}
},
{
path: '/login',
name: 'login',
component: Login
},
]
})
給mainPage首頁路由增加了 requiresAuth,所以我們需要在mainPage頁面里面使用路由鉤子beforeRouteEnter來攔截路由,
路由攔截兩個作用:
1.獲取用戶信息
由于每次刷新頁面store里面的信息會清空,因此我們可以在beforeRouteEnter增加獲取用戶信息接口,這樣每次清空store信息后我們再去調(diào)取接口重新獲取存入store
2.登陸攔截
根據(jù)當(dāng)前路由的requireAuth字段判斷當(dāng)前頁面是否需要登錄驗證,若需要登錄驗證,查看localStroage里有無Token ,有就繼續(xù)執(zhí)行,如果沒有Token ,重定向到登錄頁。
// mainPage頁面
import LeftNav from "@/components/leftNav";
import { getLoginInfo, logout } from "@/api/getData";
import store from "@/store";
export default {
components: { LeftNav },
// 路由攔截
async beforeRouteEnter(to, from, next) {
// 因為刷新頁面每次都會清空strore里面的信息,因此每次刷新我們先調(diào)用獲取用戶信息接口獲取用戶信息存入vuex
let { data } = await getLoginInfo();
store.commit("setLoginInfo", data);
//判斷是否需要登錄驗證
let token = window.localStorage.getItem("token");
if (to.meta.requiresAuth) {
if (token) {
next();
} else {
// 調(diào)取退出接口,將頁面路由重定向到登錄頁進(jìn)行登錄
logOut()
next({
path: "/login",
query: { redirect: to.fullPath }
});
}
} else {
next();
}
}
};
四、Vuex配置
上面我們調(diào)用用戶信息接口時用到了store進(jìn)行存儲,因此需要配置
// store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
loginInfo: {},
},
mutations: {
setLoginInfo(state, data) {
state.loginInfo = data;
},
},
actions: {},
modules: {}
})
五、login頁面登錄
這里我寫了個登錄組件,下面是點擊登錄時的 handleSubmit 方法,登錄成功后我們需要把后端返回的token存入localStrage里頭,跳轉(zhuǎn)到首頁之前重定向過來的頁面。
async handleSubmit() {
// 先校驗表單輸入
let flag = await this.$refs.loginForm.validate();
if (!flag) return;
try {
this.loading = true;
// 調(diào)用登錄接口
let { msg } = await login({
userName: this.user,
password: this.password
});
this.$Message.success(msg);
window.localStorage.setItem('token', res.data.token) // 登錄成功后將后臺返回的token存到localStorage
// 跳回指定路由
let redirectUrl = decodeURIComponent(this.$route.query.redirect || "/");
this.$router.push({ path: redirectUrl });
} catch (e) {
this.$Message.warning(e);
} finally {
this.loading = false;
}
}
}
整體流程跑完了,實現(xiàn)的主要功能就是:
- 訪問登錄注冊之外的路由,都需要登錄權(quán)限,比如首頁,判斷有無token,有則訪問成功,沒有則跳轉(zhuǎn)到登錄頁面;
- 成功登錄之后,保存后端返回的token,跳轉(zhuǎn)到之前重定向過來的頁面;
- token 過期后,請求接口時,身份過期,跳轉(zhuǎn)到登錄頁,繼續(xù)第二步;
六、菜單權(quán)限
我們在首頁調(diào)用getLoginInfo接口返回了用戶信息、菜單列表和權(quán)限按鈕列表的JSON數(shù)據(jù),存入Vuex里頭,這時我們就可以在左側(cè)導(dǎo)航組件里根據(jù)后臺返回的菜單列表數(shù)據(jù)動態(tài)顯示左側(cè)導(dǎo)航菜單,從而實現(xiàn)了頁面權(quán)限
七、按鈕權(quán)限
【7.1】自定義auth指令
我們定義一個auth指令來顯示隱藏頁面元素,從而實現(xiàn)按鈕權(quán)限
// util/auth.js
import store from "@/store";
export default {
bind(el, binding, vnode) {
let auths = store.state.loginInfo.roleBtns; // 后臺返回的按鈕權(quán)限列表
let hasAuth = auths.includes(binding.value);
if (hasAuth) {
delete el.style.display;
} else {
el.style.display = "none";
}
}
}
【7.2】全局注冊

【7.3】在頁面使用
如下所示,通過自定義指令v-auth綁定字段,當(dāng)綁定的字段包含在后端返回的roleBtns列表里,那么該按鈕會顯示,否則隱藏
<div class="header-right">
<el-button v-auth="'editProduct'" type="primary" size="mini" @click="addProductRelation">新增
</el-button>
<el-button v-auth="'deleteProduct'" type="primary" size="mini" @click="deleteSelectProduct">刪除</el-button>
</div>