鏈接:https://juejin.cn/post/6844903478880370701
先說一說我權限控制的主體思路,前端會有一份路由表,它表示了每一個路由可訪問的權限。當用戶登錄之后,通過 token 獲取用戶的 role ,動態根據用戶的 role 算出其對應有權限的路由,再通過router.addRoutes
動態掛載路由。但這些控制都只是頁面級的,說白了前端再怎么做權限控制都不是絕對安全的,后端的權限驗證是逃不掉的。
我司現在就是前端來控制頁面級的權限,不同權限的用戶顯示不同的側邊欄和限制其所能進入的頁面(也做了少許按鈕級別的權限控制),后端則會驗證每一個涉及請求的操作,驗證其是否有該操作的權限,每一個后台的請求不管是 get 還是 post 都會讓前端在請求 header
里面攜帶用戶的 token,后端會根據該 token 來驗證用戶是否有權限執行該操作。若沒有權限則拋出一個對應的狀態碼,前端檢測到該狀態碼,做出相對應的操作。
權限 前端or后端 來控制?
有很多人表示他們公司的路由表是於后端根據用戶的權限動態生成的,我司不采取這種方式的原因如下:
- 項目不斷的迭代你會異常痛苦,前端新開發一個頁面還要讓后端配一下路由和權限,讓我們想了曾經前后端不分離,被后端支配的那段恐怖時間了。
- 其次,就拿我司的業務來說,雖然后端的確也是有權限驗證的,但它的驗證其實是針對業務來划分的,比如超級編輯可以發布文章,而實習編輯只能編輯文章不能發布,但對於前端來說不管是超級編輯還是實習編輯都是有權限進入文章編輯頁面的。所以前端和后端權限的划分是不太一致。
- 還有一點是就vue2.2.0之前異步掛載路由是很麻煩的一件事!不過好在官方也出了新的api,雖然本意是來解決ssr的痛點的。。。
addRoutes
在之前通過后端動態返回前端路由一直很難做的,因為vue-router必須是要vue在實例化之前就掛載上去的,不太方便動態改變。不過好在vue2.2.0以后新增了router.addRoutes
動態添加更多路由到路由器。參數必須是一個數組,使用與routes構造函數選項相同的路由配置格式。
有了這個我們就可相對方便的做權限控制了
(下面代碼都是 vue element admin 中的,請下載自看)
具體實現
- 創建vue實例的時候將vue-router掛載,但這個時候vue-router掛載一些登錄或者不用權限的公用的頁面。
- 當用戶登錄后,獲取用role,將role和路由表每個頁面的需要的權限作比較,生成最終用戶可訪問的路由表。
- 調用router.addRoutes(store.getters.addRouters)添加用戶可訪問的路由。
- 使用vuex管理路由表,根據vuex中可訪問的路由渲染側邊欄組件。
router.js
首先我們實現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
// 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'); // 否則全部重定向到登錄頁 } } });
import router from '@/router' import store from '@/store' import { getToken, getUserInfo } from '@/utils/auth' import { constantRoutes } from './router' import { tip } from '@/utils/Tip/tip' import NProgress from 'nprogress' import 'nprogress/nprogress.css' NProgress.configure({ showSpinner: false }) const whiteList = ['/login', '/auth-redirect'] // no redirect whitelist router.beforeEach(async(to, from, next) => { // start progress bar NProgress.start() // determine whether the user has logged in const hasToken = getToken() if (hasToken) { if (to.path === '/login') { // if is logged in, redirect to the home page next({ path: '/index' }) NProgress.done() // hack: https://github.com/PanJiaChen/vue-element-admin/pull/2939 } else { // determine whether the user has obtained his permission roles through getInfo const hasRoles = store.getters.roles && store.getters.roles.length > 0 console.log(store.state.user.roles) console.log(hasRoles, '----------')
// 登錄玩成之后並沒有獲取 角色信息,獲取角色信息由下玩成,所以第一次進入到else if (hasRoles) { console.log(router) next() } else { try { // get user info const roles = store.state.user.roles console.log(accessRoutes, '-------------' ,roles) // const roles = store.state.user // console.log(roles) // this.$store.dispatch('../permission/generateRoutes',roles) // generate accessible routes map based on roles const accessRoutes = await store.dispatch('permission/generateRoutes', roles) // dynamically add accessible routes router.addRoutes(accessRoutes) // hack method to ensure that addRoutes is complete // set the replace: true, so the navigation will not leave a history record next({ ...to, replace: true }) } catch (error) { // remove token and go to login page to re-login await store.dispatch('user/resetToken') Message.error(error || 'Has Error') next(`/login?redirect=${to.path}`) NProgress.done() } } } } else { /* has no token*/ if (whiteList.indexOf(to.path) !== -1) { // in the free login whitelist, go directly next() } else { // other pages that do not have permission to access are redirected to the login page. next(`/login?redirect=${to.path}`) NProgress.done() } } }) router.afterEach(() => { // finish progress bar NProgress.done() })
這邊邏輯是,登錄完成之后並沒有獲取 角色信息,獲取角色信息由下玩成,所以第一次進入到else,
為什么要這么做呢?
首先,這樣子做可以將角色信息保存在 vuex 中, 如果頁面刷新 vuex 也會隨着刷新,但是獲取 角色信息都是寫在 導航前置守衛中,
所以當 vuex 中的角色信息為空時,將會根據 token 發起獲取角色的請求,這樣只要 token 是有效的,就能保證角色信息一直存在。
然后看看看后面的代碼
// generate accessible routes map based on roles const accessRoutes = await store.dispatch('permission/generateRoutes', roles) console.log(accessRoutes, '-------------') // dynamically add accessible routes router.addRoutes(accessRoutes)
后面緊跟着動態路由,這說明了 ,只要角色失效了,就會重新執行動態路由,這樣路由也不用緩存到本地了,刷新頁面動態添加的
路由也不會失效了。
這里害有一個小問題,就是 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()
來釋放鈎子,
就能確保所有的路由都已經掛在完成了。
這里再附上更加詳細的答案:
next({ ...to, replace: true }) // hack方法 確保addRoutes已完成。有大佬理解並解釋一下這句嗎?
這行代碼重新進入router.beforeEach這個鈎子,這時候再通過next()來釋放鈎子,就能確保所有的路由都已經掛在完成了。還是沒太懂
router.addRoutes是同步方法,整體流程:
1. 路由跳轉,根據目標地址從router中提取route信息,由於此時還沒addRouters,所以解析出來的route是個空的,不包含組件。
2. 執行beforeEach鈎子函數,然后內部會動態添加路由,但此時route已經生成了,不是說router.addRoutes后,這個route會自動更新,如果直接next(),最終渲染的就是空的。
3. 調用next({ ...to, replace: true }),會abort剛剛的跳轉,然后重新走一遍上述邏輯,這時從router中提取的route信息就包含組件了,之后就和正常邏輯一樣了。
主要原因就是生成route是在執行beforeEach鈎子之前。
如果沒有看懂,請去看我的《vue-router使用next()跳轉到指定路徑時會無限循環》這篇文章。只能幫到這了
store/permission.js
就來就講一講 GenerateRoutes Action
import router, { asyncRoutes, constantRoutes } from '@/router' /** * Use meta.role to determine if the current user has permission * @param roles * @param route */ function hasPermission(roles, route) { if (route.meta && route.meta.roles) { return roles.some(role => route.meta.roles.includes(role)) } else { return true } } /** * Filter asynchronous routing tables by recursion * @param routes asyncRoutes * @param roles */ export function filterAsyncRoutes(routes, roles) { const res = [] routes.forEach(route => { const tmp = { ...route } if (hasPermission(roles, tmp)) { if (tmp.children) { tmp.children = filterAsyncRoutes(tmp.children, roles) } res.push(tmp) } }) return res } const state = { routes: [], addRoutes: [] } const mutations = { SET_ROUTES: (state, routes) => { state.addRoutes = routes state.routes = constantRoutes.concat(routes) } } const actions = { generateRoutes({ commit }, roles) { return new Promise(resolve => { let accessedRoutes if (roles === '總經理') { accessedRoutes = asyncRoutes || [] // dynamically add accessible routes router.addRoutes(accessedRoutes) // router.options.routes = [...router.options.routes, ...accessedRoutes] } else { accessedRoutes = filterAsyncRoutes(asyncRoutes, roles) } commit('SET_ROUTES', accessedRoutes) resolve(accessedRoutes) }) } } export default { namespaced: true, state, mutations, actions }
由於我這邊接口角色和token登錄一起獲取的,所以里面還動態添加了一下路由,現實不應這么寫
這里的代碼說白了就是干了一件事,通過用戶的權限和之前在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一直指向當前路由就可以了,就是這么簡單
按鈕級別權限控制