Vue3純前端實現(xiàn)Vue路由權限的方法詳解
前言
在開發(fā)管理后臺時,都會存在多個角色登錄,登錄成功后,不同的角色會展示不同的菜單路由。這就是我們通常所說的動態(tài)路由權限,實現(xiàn)路由權限的方案有多種,比較常用的是由前端使用addRoutes(V3版本改成了addRoute)動態(tài)掛載路由和服務端返回可訪問的路由菜單這兩種。今天主要是從前端角度,實現(xiàn)路由權限的功能。
RBAC模型
前端實現(xiàn)路由權限主要是基于RBAC模型。
RBAC(Role-Based Access Control)即:基于角色的權限控制。通過角色關聯(lián)用戶,角色關聯(lián)權限的方式間接賦予用戶權限。
代碼實現(xiàn)
登錄
首先是登錄,登錄成功后,服務端會返回用戶登錄的角色、token以及用戶信息等。用戶角色如:role: ['admin']。我們一般會將這些信息保存到Vuex里。
const login = () => {
ruleFormRef.value?.validate((valid: boolean) => {
if (valid) {
store.dispatch('userModule/login', { ...accountForm })
} else {
console.log('error submit!')
}
})
}信息存儲在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('/')
},服務端返回token:

服務端返回用戶信息:

菜單信息
路由菜單信息分為兩種,一種是默認路由constantRoutes,即所有人都能夠訪問的頁面,不需去通過用戶角色去判斷,如login、404、首頁等等。還有一種就是動態(tài)路由asyncRoutes,用來放置有權限(roles 屬性)的路由,這部分的路由是需要訪問權限的。我們最終將在動態(tài)路由里面根據用戶角色篩選出能訪問的動態(tài)路由列表。
我們將默認路由和動態(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)計', hidden: false }
},
{
path: '/analysis/overview',
name: 'Overview',
component: () => import('@/views/analysis/overview/overview.vue'),
meta: { title: '核心技術', 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'
}
}
]
/**
* 動態(tài)路由
* 用來放置有權限(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)管理這個菜單作為動態(tài)路由部分,里面的子菜單meta屬性下都分配有一個訪問權限的role屬性,我們需要將role屬性和用戶角色去匹配是否用戶具有訪問權限。
動態(tài)路由篩選
思路:
我們登錄得到了用戶角色role和寫好路由信息(分為默認路由列表和動態(tài)路由列表),之后我們需要做的就是通過用戶角色role去匹配動態(tài)路由列表里面每個子路由的role屬性,得到能夠訪問的動態(tài)路由部分,將默認路由和我們得到的動態(tài)路由進行拼接這樣我們就得到了用戶能夠訪問的完整前端路由,最后使用addRoute將完整路由掛載到router上。
有了這樣一個比較清晰的思路,接下來我們就來嘗試著實現(xiàn)它。
我們可以將這塊的邏輯也放在Vuex里面,在store/modules下新建一個permission.ts文件。
首先我們需要寫一個方法去判斷用戶是否具有訪問單個路由的權限:
/**
* 判斷用戶是否有權限訪問單個路由
* 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
}
}實現(xiàn)的核心是route.meta.roles.includes(role),即路由的roles是否包含了用戶的角色,包含了就可以訪問,否則不能。
對用戶角色進行some遍歷主要是用戶的角色可能存在多個,如:['admin', 'editor']。
這樣我們就實現(xiàn)了單個路由訪問權限的篩選,但是動態(tài)路由列表是一個數組,每個一級路由下可能有二級路由、三級路由甚至更多,這樣我們就需要用到遞歸函數進行篩選:
/**
* 篩選可訪問的動態(tài)路由
* roles:用戶角色
* route:訪問的動態(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
}這樣,通過調用filterAsyncRoutes這個函數,然后傳入utes:動態(tài)路由列表,roles:用戶角色兩個參數就能得到我們能訪問的動態(tài)路由了。
然后我們將篩選得到的動態(tài)路由和默認路由通過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'
/**
* 判斷用戶是否有權限訪問單個路由
* 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
}
}
/**
* 篩選可訪問的動態(tài)路由
* roles:用戶角色
* route:訪問的動態(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: 篩選出的動態(tài)路由
const accessedRoutes = filterAsyncRoutes(asyncRoutes, roles)
// 將accessedRoutes和默認路由constantRoutes拼接得到完整可訪問路由
commit('SET_ROUTES', constantRoutes.concat(accessedRoutes))
commit('SET_DYNAMICROUTES', accessedRoutes)
// 通過addRoute將路由掛載到router上
accessedRoutes.forEach((route) => {
router.addRoute(route)
})
}
}
}這樣就實現(xiàn)了所有代碼邏輯。有個問題,addRoute應該何時調用,在哪里調用?
登錄后,獲取用戶的權限信息,然后篩選有權限訪問的路由,再調用addRoute添加路由。這個方法是可行的。但是不可能每次進入應用都需要登錄,用戶刷新瀏覽器又要登錄一次。所以addRoute還是要在全局路由守衛(wèi)里進行調用。
我們在router文件夾下創(chuàng)建一個permission.ts,用于寫全局路由守衛(wèi)相關邏輯:
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') {
// 如果登錄,并準備進入 login 頁面,則重定向到主頁
next({ path: '/' })
NProgress.done()
} else {
const roles = store.state.userModule.roles
store.dispatch('routesModule/generateRoutes', { roles })
// 確保添加路由已完成
// 設置 replace: true, 因此導航將不會留下歷史記錄
next({ ...to, replace: true })
// next()
}
} else {
// 如果沒有 token
if (whiteList.includes(to.path)) {
// 如果在免登錄的白名單中,則直接進入
next()
} else {
// 其他沒有訪問權限的頁面將被重定向到登錄頁面
next('/login')
NProgress.done()
}
}
}
)
router.afterEach(() => {
NProgress.done()
})這樣,完整的路由權限功能就完成了。我們可以做一下驗證:

動態(tài)路由
我們登錄的用戶角色為roles: ['editor'],動態(tài)路由為系統(tǒng)管理菜單,里面有四個子路由對應有roles,正常情況下我們可以訪問系統(tǒng)管理菜單下的角色管理和用戶管理。

渲染菜單界面

篩選出的動態(tài)路由
沒有任何問題!
總結
前端實現(xiàn)動態(tài)路由是基于RBAC思想,通過用戶角色去篩選出可以訪問的路由掛載在router上。這樣實現(xiàn)有一點不好的地方在于菜單信息是寫死在前端,以后要改個顯示文字或權限信息,需要重新修改然后編譯。
到此這篇關于Vue3純前端實現(xiàn)Vue路由權限的文章就介紹到這了,更多相關Vue3純前端實現(xiàn)路由權限內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
vue輪播組件實現(xiàn)$children和$parent 附帶好用的gif錄制工具
這篇文章主要介紹了vue輪播組件實現(xiàn),$children和$parent,附帶好用的gif錄制工具,需要的朋友可以參考下2019-09-09
Vue.js 中取得后臺原生HTML字符串 原樣顯示問題的解決方法
這篇文章主要介紹了VUE.js 中取得后臺原生HTML字符串 原樣顯示問題 ,本文給大家介紹的非常詳細,具有一定的參考借鑒價值,需要的朋友可以參考下2018-06-06

