完整項目地址:vue-element-adminhtml
系列文章:前端
拖更有點嚴重,過了半個月才寫了第二篇教程。無奈本身是一個業務猿,天天被我司的產品虐的死去活來,以前又病了一下休息了幾天,你們見諒。vue
進入正題,作後臺項目區別於作其它的項目,權限驗證與安全性是很是重要的,能夠說是一個後臺項目一開始就必須考慮和搭建的基礎核心功能。咱們所要作到的是:不一樣的權限對應着不一樣的路由,同時側邊欄也需根據不一樣的權限,異步生成。這裏先簡單說一下,我實現登陸和權限驗證的思路。webpack
上述全部的數據和操做都是經過vuex全局管理控制的。(補充說明:刷新頁面後 vuex的內容也會丟失,因此須要重複上述的那些操做)接下來,咱們一塊兒手摸手一步一步實現這個系統。ios
首先咱們無論什麼權限,來實現最基礎的登陸功能。git
隨便找一個空白頁面擼上兩個input的框,一個是登陸帳號,一個是登陸密碼。再放置一個登陸按鈕。咱們將登陸按鈕上綁上click事件,點擊登陸以後向服務端提交帳號和密碼進行驗證。 這就是一個最簡單的登陸頁面。若是你以爲還要寫的更加完美點,你能夠在向服務端提交以前對帳號和密碼作一次簡單的校驗。詳細代碼github
click事件觸發登陸操做:web
this.$store.dispatch('LoginByUsername', this.loginForm).then(() => {
this.$router.push({ path: '/' }); //登陸成功以後重定向到首頁
}).catch(err => {
this.$message.error(err); //登陸失敗提示錯誤
});
複製代碼
action:vue-router
LoginByUsername({ commit }, userInfo) {
const username = userInfo.username.trim()
return new Promise((resolve, reject) => {
loginByUsername(username, userInfo.password).then(response => {
const data = response.data
Cookies.set('Token', response.data.token) //登陸成功後將token存儲在cookie之中
commit('SET_TOKEN', data.token)
resolve()
}).catch(error => {
reject(error)
});
});
}
複製代碼
登陸成功後,服務端會返回一個 token(該token的是一個能惟一標示用戶身份的一個key),以後咱們將token存儲在本地cookie之中,這樣下次打開頁面或者刷新頁面的時候能記住用戶的登陸狀態,不用再去登陸頁面從新登陸了。vuex
ps:爲了保證安全性,我司如今後臺全部token有效期(Expires/Max-Age)都是Session,就是當瀏覽器關閉了就丟失了。從新打開遊覽器都須要從新登陸驗證,後端也會在每週固定一個時間點從新刷新token,讓後臺用戶所有從新登陸一次,確保後臺用戶不會由於電腦遺失或者其它緣由被人隨意使用帳號。
用戶登陸成功以後,咱們會在全局鉤子router.beforeEach
中攔截路由,判斷是否已得到token,在得到token以後咱們就要去獲取用戶的基本信息了
//router.beforeEach
if (store.getters.roles.length === 0) { // 判斷當前用戶是否已拉取完user_info信息
store.dispatch('GetInfo').then(res => { // 拉取user_info
const roles = res.data.role;
next();//resolve 鉤子
})
複製代碼
就如前面所說的,我只在本地存儲了一個用戶的token,並無存儲別的用戶信息(如用戶權限,用戶名,用戶頭像等)。有些人會問爲何不把一些其它的用戶信息也存一下?主要出於以下的考慮:
假設我把用戶權限和用戶名也存在了本地,但我這時候用另外一臺電腦登陸修改了本身的用戶名,以後再用這臺存有以前用戶信息的電腦登陸,它默認會去讀取本地 cookie 中的名字,並不會去拉去新的用戶信息。
因此如今的策略是:頁面會先從 cookie 中查看是否存有 token,沒有,就走一遍上一部分的流程從新登陸,若是有token,就會把這個 token 返給後端去拉取user_info,保證用戶信息是最新的。 固然若是是作了單點登陸得功能的話,用戶信息存儲在本地也是能夠的。當你一臺電腦登陸時,另外一臺會被提下線,因此總會從新登陸獲取最新的內容。
並且從代碼層面我建議仍是把 login
和get_user_info
兩件事分開比較好,在這個後端全面微服務的年代,後端同窗也想寫優雅的代碼~
先說一說我權限控制的主體思路,前端會有一份路由表,它表示了每個路由可訪問的權限。當用戶登陸以後,經過 token 獲取用戶的 role ,動態根據用戶的 role 算出其對應有權限的路由,再經過router.addRoutes
動態掛載路由。但這些控制都只是頁面級的,說白了前端再怎麼作權限控制都不是絕對安全的,後端的權限驗證是逃不掉的。
我司如今就是前端來控制頁面級的權限,不一樣權限的用戶顯示不一樣的側邊欄和限制其所能進入的頁面(也作了少量按鈕級別的權限控制),後端則會驗證每個涉及請求的操做,驗證其是否有該操做的權限,每個後臺的請求無論是 get 仍是 post 都會讓前端在請求 header
裏面攜帶用戶的 token,後端會根據該 token 來驗證用戶是否有權限執行該操做。若沒有權限則拋出一個對應的狀態碼,前端檢測到該狀態碼,作出相對應的操做。
有不少人表示他們公司的路由表是於後端根據用戶的權限動態生成的,我司不採起這種方式的緣由以下:
在以前經過後端動態返回前端路由一直很難作的,由於vue-router必須是要vue在實例化以前就掛載上去的,不太方便動態改變。不過好在vue2.2.0之後新增了router.addRoutes
Dynamically add more routes to the router. The argument must be an Array using the same route config format with the routes constructor option.
有了這個咱們就可相對方便的作權限控制了。(樓主以前在權限控制也走了很多歪路,能夠在項目的commit記錄中看到,重構了不少次,最先沒用addRoute整個權限控制代碼裏都是各類if/else的邏輯判斷,代碼至關的耦合和複雜)
首先咱們實現router.js路由表,這裏就拿前端控制路由來舉例(後端存儲的也差很少,稍微改造一下就行了)
// router.js
import Vue from 'vue';
import Router from 'vue-router';
import Login from '../views/login/';
const dashboard = resolve => require(['../views/dashboard/index'], resolve);
//使用了vue-routerd的[Lazy Loading Routes
](https://router.vuejs.org/en/advanced/lazy-loading.html)
//全部權限通用路由表
//如首頁和登陸頁和一些不用權限的公用頁面
export const constantRouterMap = [
{ path: '/login', component: Login },
{
path: '/',
component: Layout,
redirect: '/dashboard',
name: '首頁',
children: [{ path: 'dashboard', component: dashboard }]
},
]
//實例化vue的時候只掛載constantRouter
export default new Router({
routes: constantRouterMap
});
//異步掛載的路由
//動態須要根據權限加載的路由表
export const asyncRouterMap = [
{
path: '/permission',
component: Layout,
name: '權限測試',
meta: { role: ['admin','super_editor'] }, //頁面須要的權限
children: [
{
path: 'index',
component: Permission,
name: '權限測試頁',
meta: { role: ['admin','super_editor'] } //頁面須要的權限
}]
},
{ path: '*', redirect: '/404', hidden: true }
];
複製代碼
這裏咱們根據 vue-router官方推薦 的方法經過meta標籤來標示改頁面能訪問的權限有哪些。如meta: { role: ['admin','super_editor'] }
表示該頁面只有admin和超級編輯纔能有資格進入。
注意事項:這裏有一個須要很是注意的地方就是 404
頁面必定要最後加載,若是放在constantRouterMap
一同聲明瞭404
,後面的因此頁面都會被攔截到404
,詳細的問題見addRoutes when you've got a wildcard route for 404s does not work
關鍵的main.js
// main.js
router.beforeEach((to, from, next) => {
if (store.getters.token) { // 判斷是否有token
if (to.path === '/login') {
next({ path: '/' });
} else {
if (store.getters.roles.length === 0) { // 判斷當前用戶是否已拉取完user_info信息
store.dispatch('GetInfo').then(res => { // 拉取info
const roles = res.data.role;
store.dispatch('GenerateRoutes', { roles }).then(() => { // 生成可訪問的路由表
router.addRoutes(store.getters.addRouters) // 動態添加可訪問路由表
next({ ...to, replace: true }) // hack方法 確保addRoutes已完成 ,set the replace: true so the navigation will not leave a history record
})
}).catch(err => {
console.log(err);
});
} else {
next() //當有用戶權限的時候,說明全部可訪問路由已生成 如訪問沒權限的全面會自動進入404頁面
}
}
} else {
if (whiteList.indexOf(to.path) !== -1) { // 在免登陸白名單,直接進入
next();
} else {
next('/login'); // 不然所有重定向到登陸頁
}
}
});
複製代碼
這裏的router.beforeEach也結合了上一章講的一些登陸邏輯代碼。
addRoutes
方法以前的權限判斷,很是的繁瑣,由於我是把全部的路由都掛在了上去,全部我要各類判斷當前的用戶是否有權限進入該頁面,各類
if/else
的嵌套,維護起來至關的困難。但如今有了
addRoutes
以後就很是的方便,我只掛載了用戶有權限進入的頁面,沒權限,路由自動幫我跳轉的
404
,省去了很多的判斷。
這裏還有一個小hack的地方,就是router.addRoutes
以後的next()
可能會失效,由於可能next()
的時候路由並無徹底add完成,好在查閱文檔發現
next('/') or next({ path: '/' }): redirect to a different location. The current navigation will be aborted and a new one will be started.
這樣咱們就能夠簡單的經過next(to)
巧妙的避開以前的那個問題了。這行代碼從新進入router.beforeEach
這個鉤子,這時候再經過next()
來釋放鉤子,就能確保全部的路由都已經掛在完成了。
就來就講一講 GenerateRoutes Action
// store/permission.js
import { asyncRouterMap, constantRouterMap } from 'src/router';
function hasPermission(roles, route) {
if (route.meta && route.meta.role) {
return roles.some(role => route.meta.role.indexOf(role) >= 0)
} else {
return true
}
}
const permission = {
state: {
routers: constantRouterMap,
addRouters: []
},
mutations: {
SET_ROUTERS: (state, routers) => {
state.addRouters = routers;
state.routers = constantRouterMap.concat(routers);
}
},
actions: {
GenerateRoutes({ commit }, data) {
return new Promise(resolve => {
const { roles } = data;
const accessedRouters = asyncRouterMap.filter(v => {
if (roles.indexOf('admin') >= 0) return true;
if (hasPermission(roles, v)) {
if (v.children && v.children.length > 0) {
v.children = v.children.filter(child => {
if (hasPermission(roles, child)) {
return child
}
return false;
});
return v
} else {
return v
}
}
return false;
});
commit('SET_ROUTERS', accessedRouters);
resolve();
})
}
}
};
export default permission;
複製代碼
這裏的代碼說白了就是幹了一件事,經過用戶的權限和以前在router.js裏面asyncRouterMap的每個頁面所須要的權限作匹配,最後返回一個該用戶可以訪問路由有哪些。
最後一個涉及到權限的地方就是側邊欄,不過在前面的基礎上已經很方便就能實現動態顯示側邊欄了。這裏側邊欄基於element-ui的NavMenu來實現的。 代碼有點多不貼詳細的代碼了,有興趣的能夠直接去github上看地址,或者直接看關於側邊欄的文檔。
說白了就是遍歷以前算出來的permission_routers
,經過vuex拿到以後動態v-for渲染而已。不過這裏由於有一些業務需求因此加了不少判斷 好比咱們在定義路由的時候會加不少參數
/**
* hidden: true if `hidden:true` will not show in the sidebar(default is false)
* redirect: noredirect if `redirect:noredirect` will no redirct in the breadcrumb
* name:'router-name' the name is used by <keep-alive> (must set!!!)
* meta : {
role: ['admin','editor'] will control the page role (you can set multiple roles)
title: 'title' the name show in submenu and breadcrumb (recommend set)
icon: 'svg-name' the icon show in the sidebar,
noCache: true if fasle ,the page will no be cached(default is false)
}
**/
複製代碼
這裏僅供參考,並且本項目爲了支持無限嵌套路由,全部側邊欄這塊使用了遞歸組件。如須要請你們自行改造,來打造知足本身業務需求的側邊欄。
側邊欄高亮問題:不少人在羣裏問爲何本身的側邊欄不能跟着本身的路由高亮,其實很簡單,element-ui官方已經給了default-active
因此咱們只要
:default-active="$route.path" 將
default-active
一直指向當前路由就能夠了,就是這麼簡單
有不少人一直在問關於按鈕級別粒度的權限控制怎麼作。我司如今是這樣的,真正須要按鈕級別控制的地方不是不少,如今是經過獲取到用戶的role以後,在前端用v-if手動判斷來區分不一樣權限對應的按鈕的。理由前面也說了,我司顆粒度的權限判斷是交給後端來作的,每一個操做後端都會進行權限判斷。並且我以爲其實前端真正須要按鈕級別判斷的地方不是不少,若是一個頁面有不少種不一樣權限的按鈕,我以爲更多的應該是考慮產品層面是否設計合理。固然你強行說我想作按鈕級別的權限控制,你也能夠參照路由層面的作法,搞一個操做權限表。。。但我的以爲有點畫蛇添足。或者將它封裝成一個指令都是能夠的。
這裏再說一說 axios 吧。雖然在上一篇系列文章中簡單介紹過,不過這裏仍是要在嘮叨一下。如上文所說,我司服務端對每個請求都會驗證權限,因此這裏咱們針對業務封裝了一下請求。首先咱們經過request攔截器在每一個請求頭裏面塞入token,好讓後端對請求進行權限驗證。並建立一個respone攔截器,當服務端返回特殊的狀態碼,咱們統一作處理,如沒權限或者token失效等操做。
import axios from 'axios'
import { Message } from 'element-ui'
import store from '@/store'
import { getToken } from '@/utils/auth'
// 建立axios實例
const service = axios.create({
baseURL: process.env.BASE_API, // api的base_url
timeout: 5000 // 請求超時時間
})
// request攔截器
service.interceptors.request.use(config => {
// Do something before request is sent
if (store.getters.token) {
config.headers['X-Token'] = getToken() // 讓每一個請求攜帶token--['X-Token']爲自定義key 請根據實際狀況自行修改
}
return config
}, error => {
// Do something with request error
console.log(error) // for debug
Promise.reject(error)
})
// respone攔截器
service.interceptors.response.use(
response => response,
/** * 下面的註釋爲經過response自定義code來標示請求狀態,當code返回以下狀況爲權限有問題,登出並返回到登陸頁 * 如經過xmlhttprequest 狀態碼標識 邏輯可寫在下面error中 */
// const res = response.data;
// if (res.code !== 20000) {
// Message({
// message: res.message,
// type: 'error',
// duration: 5 * 1000
// });
// // 50008:非法的token; 50012:其餘客戶端登陸了; 50014:Token 過時了;
// if (res.code === 50008 || res.code === 50012 || res.code === 50014) {
// MessageBox.confirm('你已被登出,能夠取消繼續留在該頁面,或者從新登陸', '肯定登出', {
// confirmButtonText: '從新登陸',
// cancelButtonText: '取消',
// type: 'warning'
// }).then(() => {
// store.dispatch('FedLogOut').then(() => {
// location.reload();// 爲了從新實例化vue-router對象 避免bug
// });
// })
// }
// return Promise.reject('error');
// } else {
// return response.data;
// }
error => {
console.log('err' + error)// for debug
Message({
message: error.message,
type: 'error',
duration: 5 * 1000
})
return Promise.reject(error)
})
export default service
複製代碼
文章一開始也說了,後臺的安全性是很重要的,簡簡單單的一個帳號+密碼的方式是很難保證安全性的。因此我司的後臺項目都是用了兩步驗證的方式,以前咱們也嘗試過使用基於 google-authenticator 或者youbikey
這樣的方式但難度和操做成本都比較大。後來仍是準備藉助騰訊爸爸,這年代誰不用微信。。。安全性騰訊爸爸也幫我作好了保障。 樓主建議兩步驗證要支持多個渠道不要只微信或者QQ,前段時間QQ第三方登陸就出了bug,官方兩三天才修好的,害我背了鍋/(ㄒoㄒ)/~~ 。
這裏的兩部驗證有點名存實亡,其實就是帳號密碼驗證過以後還須要一個綁定的第三方平臺登陸驗證而已。 寫起來也很簡單,在原有登陸得邏輯上改造一下就好。
this.$store.dispatch('LoginByEmail', this.loginForm).then(() => {
//this.$router.push({ path: '/' });
//不重定向到首頁
this.showDialog = true //彈出選擇第三方平臺的dialog
}).catch(err => {
this.$message.error(err); //登陸失敗提示錯誤
});
複製代碼
登陸成功以後不直接跳到首頁而是讓用戶兩步登陸,選擇登陸得平臺。 接下來就是全部第三方登陸同樣的地方經過 OAuth2.0 受權。這個各大平臺大同小異,你們自行查閱文檔,不展開了,就說一個微信受權比較坑的地方。注意你連參數的順序都不能換,否則會驗證不經過。具體代碼,同時我也封裝了openWindow方法你們自行看吧。 當第三方受權成功以後都會跳到一個你以前有一個傳入redirect——uri的頁面
因此咱們後臺也須要開一個authredirect頁面:代碼。他的做用是第三方登陸成功以後會默認跳到受權的頁面,受權的頁面會再次重定向回咱們的後臺,因爲是spa,改變路由的體驗很差,咱們經過window.opener.location.href
的方式改變hash,在login.js裏面再監聽hash的變化。當hash變化時,獲取以前第三方登陸成功返回的code與第一步帳號密碼登陸以後返回的uid一同發送給服務端驗證是否正確,若是正確,這時候就是真正的登陸成功。
created() {
window.addEventListener('hashchange', this.afterQRScan);
},
destroyed() {
window.removeEventListener('hashchange', this.afterQRScan);
},
afterQRScan() {
const hash = window.location.hash.slice(1);
const hashObj = getQueryObject(hash);
const originUrl = window.location.origin;
history.replaceState({}, '', originUrl);
const codeMap = {
wechat: 'code',
tencent: 'code'
};
const codeName = hashObj[codeMap[this.auth_type]];
this.$store.dispatch('LoginByThirdparty', codeName).then(() => {
this.$router.push({
path: '/'
});
});
}
複製代碼
到這裏涉及登陸權限的東西也差很少講完了,這裏樓主只是給了你們一個實現的思路(都是樓主不斷摸索的血淚史),每一個公司實現的方案都有些出入,請謹慎選擇適合本身業務形態的解決方案。若是有什麼想法或者建議歡迎去本項目下留言,一同討論。
常規佔坑,這裏是手摸手,帶你用vue擼後臺系列。
完整項目地址:vue-element-admin