Vue3純前端實(shí)現(xiàn)Vue路由權(quán)限的方法詳解
前言
在開(kāi)發(fā)管理后臺(tái)時(shí),都會(huì)存在多個(gè)角色登錄,登錄成功后,不同的角色會(huì)展示不同的菜單路由。這就是我們通常所說(shuō)的動(dòng)態(tài)路由權(quán)限,實(shí)現(xiàn)路由權(quán)限的方案有多種,比較常用的是由前端使用addRoutes(V3版本改成了addRoute)動(dòng)態(tài)掛載路由和服務(wù)端返回可訪(fǎng)問(wèn)的路由菜單這兩種。今天主要是從前端角度,實(shí)現(xiàn)路由權(quán)限的功能。
RBAC模型
前端實(shí)現(xiàn)路由權(quán)限主要是基于RBAC模型。
RBAC(Role-Based Access Control)即:基于角色的權(quán)限控制。通過(guò)角色關(guān)聯(lián)用戶(hù),角色關(guān)聯(lián)權(quán)限的方式間接賦予用戶(hù)權(quán)限。
代碼實(shí)現(xiàn)
登錄
首先是登錄,登錄成功后,服務(wù)端會(huì)返回用戶(hù)登錄的角色、token以及用戶(hù)信息等。用戶(hù)角色如: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)
// 獲取用戶(hù)信息
const userInfo = await getUserInfo(data.id)
commit('SET_USERINFO', userInfo.data)
localCache.setCache('userInfo', userInfo.data)
router.replace('/')
},服務(wù)端返回token:

服務(wù)端返回用戶(hù)信息:

菜單信息
路由菜單信息分為兩種,一種是默認(rèn)路由constantRoutes,即所有人都能夠訪(fǎng)問(wèn)的頁(yè)面,不需去通過(guò)用戶(hù)角色去判斷,如login、404、首頁(yè)等等。還有一種就是動(dòng)態(tài)路由asyncRoutes,用來(lái)放置有權(quán)限(roles 屬性)的路由,這部分的路由是需要訪(fǎng)問(wèn)權(quán)限的。我們最終將在動(dòng)態(tài)路由里面根據(jù)用戶(hù)角色篩選出能訪(fǎng)問(wèn)的動(dòng)態(tài)路由列表。
我們將默認(rèn)路由和動(dòng)態(tài)路由都寫(xiě)在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: '商品類(lèi)別', 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)路由
* 用來(lá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: '部門(mén)管理', 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: '用戶(hù)管理', hidden: false, role: ['editor'] }
}
]
}
]
const router = createRouter({
history: createWebHashHistory(),
routes: constantRoutes
})
export default router我們將系統(tǒng)管理這個(gè)菜單作為動(dòng)態(tài)路由部分,里面的子菜單meta屬性下都分配有一個(gè)訪(fǎng)問(wèn)權(quán)限的role屬性,我們需要將role屬性和用戶(hù)角色去匹配是否用戶(hù)具有訪(fǎng)問(wèn)權(quán)限。
動(dòng)態(tài)路由篩選
思路:
我們登錄得到了用戶(hù)角色role和寫(xiě)好路由信息(分為默認(rèn)路由列表和動(dòng)態(tài)路由列表),之后我們需要做的就是通過(guò)用戶(hù)角色role去匹配動(dòng)態(tài)路由列表里面每個(gè)子路由的role屬性,得到能夠訪(fǎng)問(wèn)的動(dòng)態(tài)路由部分,將默認(rèn)路由和我們得到的動(dòng)態(tài)路由進(jìn)行拼接這樣我們就得到了用戶(hù)能夠訪(fǎng)問(wèn)的完整前端路由,最后使用addRoute將完整路由掛載到router上。
有了這樣一個(gè)比較清晰的思路,接下來(lái)我們就來(lái)嘗試著實(shí)現(xiàn)它。
我們可以將這塊的邏輯也放在Vuex里面,在store/modules下新建一個(gè)permission.ts文件。
首先我們需要寫(xiě)一個(gè)方法去判斷用戶(hù)是否具有訪(fǎng)問(wèn)單個(gè)路由的權(quán)限:
/**
* 判斷用戶(hù)是否有權(quán)限訪(fǎng)問(wèn)單個(gè)路由
* roles:用戶(hù)角色
* route:訪(fǎng)問(wèn)的路由
*/
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是否包含了用戶(hù)的角色,包含了就可以訪(fǎng)問(wèn),否則不能。
對(duì)用戶(hù)角色進(jìn)行some遍歷主要是用戶(hù)的角色可能存在多個(gè),如:['admin', 'editor']。
這樣我們就實(shí)現(xiàn)了單個(gè)路由訪(fǎng)問(wèn)權(quán)限的篩選,但是動(dòng)態(tài)路由列表是一個(gè)數(shù)組,每個(gè)一級(jí)路由下可能有二級(jí)路由、三級(jí)路由甚至更多,這樣我們就需要用到遞歸函數(shù)進(jìn)行篩選:
/**
* 篩選可訪(fǎng)問(wèn)的動(dòng)態(tài)路由
* roles:用戶(hù)角色
* route:訪(fǎng)問(wèn)的動(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
}這樣,通過(guò)調(diào)用filterAsyncRoutes這個(gè)函數(shù),然后傳入utes:動(dòng)態(tài)路由列表,roles:用戶(hù)角色兩個(gè)參數(shù)就能得到我們能訪(fǎng)問(wèn)的動(dòng)態(tài)路由了。
然后我們將篩選得到的動(dòng)態(tài)路由和默認(rèn)路由通過(guò)concat拼接得到完整可訪(fǎng)問(wèn)路由,最后通過(guò)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'
/**
* 判斷用戶(hù)是否有權(quán)限訪(fǎng)問(wèn)單個(gè)路由
* roles:用戶(hù)角色
* route:訪(fǎng)問(wèn)的路由
*/
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
}
}
/**
* 篩選可訪(fǎng)問(wèn)的動(dòng)態(tài)路由
* roles:用戶(hù)角色
* route:訪(fǎng)問(wèn)的動(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拼接得到完整可訪(fǎng)問(wèn)路由
commit('SET_ROUTES', constantRoutes.concat(accessedRoutes))
commit('SET_DYNAMICROUTES', accessedRoutes)
// 通過(guò)addRoute將路由掛載到router上
accessedRoutes.forEach((route) => {
router.addRoute(route)
})
}
}
}這樣就實(shí)現(xiàn)了所有代碼邏輯。有個(gè)問(wèn)題,addRoute應(yīng)該何時(shí)調(diào)用,在哪里調(diào)用?
登錄后,獲取用戶(hù)的權(quán)限信息,然后篩選有權(quán)限訪(fǎng)問(wèn)的路由,再調(diào)用addRoute添加路由。這個(gè)方法是可行的。但是不可能每次進(jìn)入應(yīng)用都需要登錄,用戶(hù)刷新瀏覽器又要登錄一次。所以addRoute還是要在全局路由守衛(wèi)里進(jìn)行調(diào)用。
我們?cè)趓outer文件夾下創(chuàng)建一個(gè)permission.ts,用于寫(xiě)全局路由守衛(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()
// 判斷該用戶(hù)是否登錄
if (token) {
if (to.path === '/login') {
// 如果登錄,并準(zhǔn)備進(jìn)入 login 頁(yè)面,則重定向到主頁(yè)
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 {
// 如果沒(méi)有 token
if (whiteList.includes(to.path)) {
// 如果在免登錄的白名單中,則直接進(jìn)入
next()
} else {
// 其他沒(méi)有訪(fǎng)問(wèn)權(quán)限的頁(yè)面將被重定向到登錄頁(yè)面
next('/login')
NProgress.done()
}
}
}
)
router.afterEach(() => {
NProgress.done()
})這樣,完整的路由權(quán)限功能就完成了。我們可以做一下驗(yàn)證:

動(dòng)態(tài)路由
我們登錄的用戶(hù)角色為roles: ['editor'],動(dòng)態(tài)路由為系統(tǒng)管理菜單,里面有四個(gè)子路由對(duì)應(yīng)有roles,正常情況下我們可以訪(fǎng)問(wèn)系統(tǒng)管理菜單下的角色管理和用戶(hù)管理。

渲染菜單界面

篩選出的動(dòng)態(tài)路由
沒(méi)有任何問(wèn)題!
總結(jié)
前端實(shí)現(xiàn)動(dòng)態(tài)路由是基于RBAC思想,通過(guò)用戶(hù)角色去篩選出可以訪(fǎng)問(wèn)的路由掛載在router上。這樣實(shí)現(xiàn)有一點(diǎn)不好的地方在于菜單信息是寫(xiě)死在前端,以后要改個(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-03
vue輪播組件實(shí)現(xiàn)$children和$parent 附帶好用的gif錄制工具
這篇文章主要介紹了vue輪播組件實(shí)現(xiàn),$children和$parent,附帶好用的gif錄制工具,需要的朋友可以參考下2019-09-09
淺談Vue開(kāi)發(fā)人員的7個(gè)最好的VSCode擴(kuò)展
這篇文章主要介紹了淺談Vue開(kāi)發(fā)人員的7個(gè)最好的VSCode擴(kuò)展,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-01-01
Vue.js 中取得后臺(tái)原生HTML字符串 原樣顯示問(wèn)題的解決方法
這篇文章主要介紹了VUE.js 中取得后臺(tái)原生HTML字符串 原樣顯示問(wèn)題 ,本文給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2018-06-06
在IDEA中Debug調(diào)試VUE項(xiàng)目的詳細(xì)步驟
idea竟然有一個(gè)神功能很多朋友都不是特別清楚,下面小編給大家?guī)?lái)了在IDEA中Debug調(diào)試VUE項(xiàng)目的詳細(xì)步驟,感興趣的朋友一起看看吧2021-10-10
解決vue中使用proxy配置不同端口和ip接口問(wèn)題
這篇文章主要介紹了解決vue中使用proxy配置不同端口和ip接口問(wèn)題 ,本文給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-08-08
webpack4打包vue前端多頁(yè)面項(xiàng)目
這篇文章主要介紹了webpack4打包vue前端多頁(yè)面項(xiàng)目的相關(guān)知識(shí),非常不錯(cuò),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2018-09-09

