Vue3純前端實(shí)現(xiàn)Vue路由權(quán)限的方法詳解
前言
在開發(fā)管理后臺(tái)時(shí),都會(huì)存在多個(gè)角色登錄,登錄成功后,不同的角色會(huì)展示不同的菜單路由。這就是我們通常所說的動(dòng)態(tài)路由權(quán)限,實(shí)現(xiàn)路由權(quán)限的方案有多種,比較常用的是由前端使用addRoutes(V3版本改成了addRoute)動(dòng)態(tài)掛載路由和服務(wù)端返回可訪問的路由菜單這兩種。今天主要是從前端角度,實(shí)現(xiàn)路由權(quán)限的功能。
RBAC模型
前端實(shí)現(xiàn)路由權(quán)限主要是基于RBAC模型。
RBAC(Role-Based Access Control)即:基于角色的權(quán)限控制。通過角色關(guān)聯(lián)用戶,角色關(guān)聯(lián)權(quán)限的方式間接賦予用戶權(quán)限。
代碼實(shí)現(xiàn)
登錄
首先是登錄,登錄成功后,服務(wù)端會(huì)返回用戶登錄的角色、token以及用戶信息等。用戶角色如:role: ['admin']。我們一般會(huì)將這些信息保存到Vuex里。
const login = () => { ruleFormRef.value?.validate((valid: boolean) => { if (valid) { store.dispatch('userModule/login', { ...accountForm }) } else { console.log('error submit!') } }) }
信息存儲(chǔ)在Vuex:
async login({ commit }, payload: IRequest) { // 登錄獲取token const { data } = await accountLogin(payload) commit('SET_TOKEN', data.token) localCache.setCache('token', data.token) // 獲取用戶信息 const userInfo = await getUserInfo(data.id) commit('SET_USERINFO', userInfo.data) localCache.setCache('userInfo', userInfo.data) router.replace('/') },
服務(wù)端返回token:
服務(wù)端返回用戶信息:
菜單信息
路由菜單信息分為兩種,一種是默認(rèn)路由constantRoutes,即所有人都能夠訪問的頁面,不需去通過用戶角色去判斷,如login、404、首頁等等。還有一種就是動(dòng)態(tài)路由asyncRoutes,用來放置有權(quán)限(roles 屬性)的路由,這部分的路由是需要訪問權(quán)限的。我們最終將在動(dòng)態(tài)路由里面根據(jù)用戶角色篩選出能訪問的動(dòng)態(tài)路由列表。
我們將默認(rèn)路由和動(dòng)態(tài)路由都寫在router/index.ts里。
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router' const Layout = () => import('@/Layout') /** 常駐路由 */ export const constantRoutes: RouteRecordRaw[] = [ { path: '/login', name: 'login', component: () => import('@/views/login/index.vue'), meta: { title: '登錄', hidden: true } }, { path: '/', component: Layout, redirect: '/analysis/dashboard', name: 'Analysis', meta: { hidden: false, icon: 'icon-home', title: '系統(tǒng)總覽' }, children: [ { path: '/analysis/dashboard', name: 'Dashboard', component: () => import('@/views/analysis/dashboard/dashboard.vue'), meta: { title: '商品統(tǒng)計(jì)', hidden: false } }, { path: '/analysis/overview', name: 'Overview', component: () => import('@/views/analysis/overview/overview.vue'), meta: { title: '核心技術(shù)', hidden: false } } ] }, { path: '/product', component: Layout, redirect: '/product/category', name: 'Product', meta: { hidden: false, icon: 'icon-tuijian', title: '商品中心' }, children: [ { path: '/product/category', name: 'Category', component: () => import('@/views/product/category/category.vue'), meta: { title: '商品類別', hidden: false } }, { path: '/product/goods', name: 'Goods', component: () => import('@/views/product/goods/goods.vue'), meta: { title: '商品信息', hidden: false } } ] }, { path: '/story', component: Layout, redirect: '/story/chat', name: 'Story', meta: { hidden: false, icon: 'icon-xiaoxi', title: '隨便聊聊' }, children: [ { path: '/story/chat', name: 'Story', component: () => import('@/views/story/chat/chat.vue'), meta: { title: '你的故事', hidden: false } }, { path: '/story/list', name: 'List', component: () => import('@/views/story/list/list.vue'), meta: { title: '故事列表', hidden: false } } ] }, { path: '/404', component: () => import('@/views/404.vue'), meta: { title: 'Not Found', hidden: true } }, { path: '/:pathMatch(.*)*', redirect: '/404', meta: { hidden: true, title: 'Not Found' } } ] /** * 動(dòng)態(tài)路由 * 用來放置有權(quán)限(roles 屬性)的路由 * 必須帶有 name 屬性 */ export const asyncRoutes: RouteRecordRaw[] = [ { path: '/system', component: Layout, redirect: '/system/department', name: 'System', meta: { hidden: false, icon: 'icon-shezhi', title: '系統(tǒng)管理' }, children: [ { path: '/system/department', name: 'Department', component: () => import('@/views/system/department/department.vue'), meta: { title: '部門管理', hidden: false, role: ['admin'] } }, { path: '/system/menu', name: 'Menu', component: () => import('@/views/system/menu/menu.vue'), meta: { title: '菜單管理', hidden: false, role: ['admin'] } }, { path: '/system/role', name: 'Role', component: () => import('@/views/system/role/role.vue'), meta: { title: '角色管理', hidden: false, role: ['editor'] } }, { path: '/system/user', name: 'User', component: () => import('@/views/system/user/user.vue'), meta: { title: '用戶管理', hidden: false, role: ['editor'] } } ] } ] const router = createRouter({ history: createWebHashHistory(), routes: constantRoutes }) export default router
我們將系統(tǒng)管理這個(gè)菜單作為動(dòng)態(tài)路由部分,里面的子菜單meta屬性下都分配有一個(gè)訪問權(quán)限的role屬性,我們需要將role屬性和用戶角色去匹配是否用戶具有訪問權(quán)限。
動(dòng)態(tài)路由篩選
思路:
我們登錄得到了用戶角色role和寫好路由信息(分為默認(rèn)路由列表和動(dòng)態(tài)路由列表),之后我們需要做的就是通過用戶角色role去匹配動(dòng)態(tài)路由列表里面每個(gè)子路由的role屬性,得到能夠訪問的動(dòng)態(tài)路由部分,將默認(rèn)路由和我們得到的動(dòng)態(tài)路由進(jìn)行拼接這樣我們就得到了用戶能夠訪問的完整前端路由,最后使用addRoute將完整路由掛載到router上。
有了這樣一個(gè)比較清晰的思路,接下來我們就來嘗試著實(shí)現(xiàn)它。
我們可以將這塊的邏輯也放在Vuex里面,在store/modules下新建一個(gè)permission.ts文件。
首先我們需要寫一個(gè)方法去判斷用戶是否具有訪問單個(gè)路由的權(quán)限:
/** * 判斷用戶是否有權(quán)限訪問單個(gè)路由 * roles:用戶角色 * route:訪問的路由 */ const hasPermission = (roles: string[], route: any) => { if (route.meta && route.meta.roles) { return roles.some((role) => { if (route.meta?.roles !== undefined) { return route.meta.roles.includes(role) } else { return false } }) } else { return true } }
實(shí)現(xiàn)的核心是route.meta.roles.includes(role),即路由的roles是否包含了用戶的角色,包含了就可以訪問,否則不能。
對(duì)用戶角色進(jìn)行some遍歷主要是用戶的角色可能存在多個(gè),如:['admin', 'editor']。
這樣我們就實(shí)現(xiàn)了單個(gè)路由訪問權(quán)限的篩選,但是動(dòng)態(tài)路由列表是一個(gè)數(shù)組,每個(gè)一級(jí)路由下可能有二級(jí)路由、三級(jí)路由甚至更多,這樣我們就需要用到遞歸函數(shù)進(jìn)行篩選:
/** * 篩選可訪問的動(dòng)態(tài)路由 * roles:用戶角色 * route:訪問的動(dòng)態(tài)列表 */ const filterAsyncRoutes = (routes: RouteRecordRaw[], roles: string[]) => { const res: RouteRecordRaw[] = [] routes.forEach((route) => { const r = { ...route } if (hasPermission(roles, r)) { if (r.children) { r.children = filterAsyncRoutes(r.children, roles) } res.push(r) } }) return res }
這樣,通過調(diào)用filterAsyncRoutes這個(gè)函數(shù),然后傳入utes:動(dòng)態(tài)路由列表,roles:用戶角色兩個(gè)參數(shù)就能得到我們能訪問的動(dòng)態(tài)路由了。
然后我們將篩選得到的動(dòng)態(tài)路由和默認(rèn)路由通過concat拼接得到完整可訪問路由,最后通過addRoute掛載。
我們將以上代碼邏輯整理到sion.ts里:
import { Module } from 'vuex' import { RouteRecordRaw } from 'vue-router' import { constantRoutes, asyncRoutes } from '@/router' import { IRootState } from '../types' import router from '@/router' /** * 判斷用戶是否有權(quán)限訪問單個(gè)路由 * roles:用戶角色 * route:訪問的路由 */ const hasPermission = (roles: string[], route: any) => { if (route.meta && route.meta.roles) { return roles.some((role) => { if (route.meta?.roles !== undefined) { return route.meta.roles.includes(role) } else { return false } }) } else { return true } } /** * 篩選可訪問的動(dòng)態(tài)路由 * roles:用戶角色 * route:訪問的動(dòng)態(tài)列表 */ const filterAsyncRoutes = (routes: RouteRecordRaw[], roles: string[]) => { const res: RouteRecordRaw[] = [] routes.forEach((route) => { const r = { ...route } if (hasPermission(roles, r)) { if (r.children) { r.children = filterAsyncRoutes(r.children, roles) } res.push(r) } }) return res } interface IPermissionState { routes: RouteRecordRaw[] dynamicRoutes: RouteRecordRaw[] } export const routesModule: Module<IPermissionState, IRootState> = { namespaced: true, state: { routes: [], dynamicRoutes: [] }, getters: {}, mutations: { SET_ROUTES(state, routes) { state.routes = routes }, SET_DYNAMICROUTES(state, routes) { state.dynamicRoutes = routes } }, actions: { generateRoutes({ commit }, { roles }) { // accessedRoutes: 篩選出的動(dòng)態(tài)路由 const accessedRoutes = filterAsyncRoutes(asyncRoutes, roles) // 將accessedRoutes和默認(rèn)路由constantRoutes拼接得到完整可訪問路由 commit('SET_ROUTES', constantRoutes.concat(accessedRoutes)) commit('SET_DYNAMICROUTES', accessedRoutes) // 通過addRoute將路由掛載到router上 accessedRoutes.forEach((route) => { router.addRoute(route) }) } } }
這樣就實(shí)現(xiàn)了所有代碼邏輯。有個(gè)問題,addRoute應(yīng)該何時(shí)調(diào)用,在哪里調(diào)用?
登錄后,獲取用戶的權(quán)限信息,然后篩選有權(quán)限訪問的路由,再調(diào)用addRoute添加路由。這個(gè)方法是可行的。但是不可能每次進(jìn)入應(yīng)用都需要登錄,用戶刷新瀏覽器又要登錄一次。所以addRoute還是要在全局路由守衛(wèi)里進(jìn)行調(diào)用。
我們?cè)趓outer文件夾下創(chuàng)建一個(gè)permission.ts,用于寫全局路由守衛(wèi)相關(guān)邏輯:
import router from '@/router' import { RouteLocationNormalized } from 'vue-router' import localCache from '@/utils/cache' import NProgress from 'nprogress' import 'nprogress/nprogress.css' import store from '@/store' NProgress.configure({ showSpinner: false }) const whiteList = ['/login'] router.beforeEach( async ( to: RouteLocationNormalized, from: RouteLocationNormalized, next: any ) => { document.title = to.meta.title as string const token: string = localCache.getCache('token') NProgress.start() // 判斷該用戶是否登錄 if (token) { if (to.path === '/login') { // 如果登錄,并準(zhǔn)備進(jìn)入 login 頁面,則重定向到主頁 next({ path: '/' }) NProgress.done() } else { const roles = store.state.userModule.roles store.dispatch('routesModule/generateRoutes', { roles }) // 確保添加路由已完成 // 設(shè)置 replace: true, 因此導(dǎo)航將不會(huì)留下歷史記錄 next({ ...to, replace: true }) // next() } } else { // 如果沒有 token if (whiteList.includes(to.path)) { // 如果在免登錄的白名單中,則直接進(jìn)入 next() } else { // 其他沒有訪問權(quán)限的頁面將被重定向到登錄頁面 next('/login') NProgress.done() } } } ) router.afterEach(() => { NProgress.done() })
這樣,完整的路由權(quán)限功能就完成了。我們可以做一下驗(yàn)證:
動(dòng)態(tài)路由
我們登錄的用戶角色為roles: ['editor'],動(dòng)態(tài)路由為系統(tǒng)管理菜單,里面有四個(gè)子路由對(duì)應(yīng)有roles,正常情況下我們可以訪問系統(tǒng)管理菜單下的角色管理和用戶管理。
渲染菜單界面
篩選出的動(dòng)態(tài)路由
沒有任何問題!
總結(jié)
前端實(shí)現(xiàn)動(dòng)態(tài)路由是基于RBAC思想,通過用戶角色去篩選出可以訪問的路由掛載在router上。這樣實(shí)現(xiàn)有一點(diǎn)不好的地方在于菜單信息是寫死在前端,以后要改個(gè)顯示文字或權(quán)限信息,需要重新修改然后編譯。
到此這篇關(guān)于Vue3純前端實(shí)現(xiàn)Vue路由權(quán)限的文章就介紹到這了,更多相關(guān)Vue3純前端實(shí)現(xiàn)路由權(quán)限內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
vue實(shí)現(xiàn)實(shí)時(shí)上傳文件進(jìn)度條
這篇文章主要為大家詳細(xì)介紹了vue實(shí)現(xiàn)實(shí)時(shí)上傳文件進(jìn)度條,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-03-03vue輪播組件實(shí)現(xiàn)$children和$parent 附帶好用的gif錄制工具
這篇文章主要介紹了vue輪播組件實(shí)現(xiàn),$children和$parent,附帶好用的gif錄制工具,需要的朋友可以參考下2019-09-09淺談Vue開發(fā)人員的7個(gè)最好的VSCode擴(kuò)展
這篇文章主要介紹了淺談Vue開發(fā)人員的7個(gè)最好的VSCode擴(kuò)展,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-01-01Vue.js 中取得后臺(tái)原生HTML字符串 原樣顯示問題的解決方法
這篇文章主要介紹了VUE.js 中取得后臺(tái)原生HTML字符串 原樣顯示問題 ,本文給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2018-06-06在IDEA中Debug調(diào)試VUE項(xiàng)目的詳細(xì)步驟
idea竟然有一個(gè)神功能很多朋友都不是特別清楚,下面小編給大家?guī)砹嗽贗DEA中Debug調(diào)試VUE項(xiàng)目的詳細(xì)步驟,感興趣的朋友一起看看吧2021-10-10