Vue 路由权限控制万字详解
·
1. 路由权限控制概述
1.1 什么是路由权限控制
路由权限控制是前端应用中确保用户只能访问其权限范围内页面的安全机制。在Vue应用中,它通过拦截路由导航、动态生成路由表等方式,实现基于用户角色或权限的访问控制。
1.2 权限控制的重要性
-
安全性:防止未授权访问敏感页面
-
用户体验:只展示用户有权访问的功能
-
数据保护:避免越权操作和数据泄露
-
合规要求:满足企业安全规范和法律法规
1.3 常见应用场景
-
管理系统中的不同角色权限(管理员、普通用户、访客)
-
SaaS平台的多租户隔离
-
内容管理系统的审核权限
-
电商平台的商家后台权限
2. 权限控制基础概念
2.1 认证 vs 授权
javascript
// 认证 (Authentication) - 验证用户身份 const isAuthenticated = checkUserLogin(username, password) // 授权 (Authorization) - 验证用户权限 const hasPermission = checkUserPermission(user.role, 'admin_panel')
2.2 常见的权限模型
RBAC (基于角色的访问控制)
javascript
// 角色 -> 权限映射
const rolePermissions = {
admin: ['*'],
editor: ['read', 'write', 'edit'],
viewer: ['read']
}
ABAC (基于属性的访问控制)
javascript
// 基于用户属性、资源属性、环境条件判断
const canAccess = (user, resource, action) => {
return user.department === resource.ownerDepartment &&
user.role === 'manager' &&
resource.sensitivity <= user.clearanceLevel
}
3. Vue Router 基础回顾
3.1 Vue Router 核心概念
javascript
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
const router = new Router({
mode: 'history',
routes: [
{
path: '/home',
name: 'Home',
component: () => import('@/views/Home.vue'),
meta: {
requiresAuth: true,
permissions: ['user']
}
}
]
})
3.2 路由元信息 (meta)
javascript
const routes = [
{
path: '/admin',
component: Admin,
meta: {
requiresAuth: true,
requiredPermissions: ['admin'],
breadcrumb: '管理后台',
keepAlive: true
}
}
]
4. 路由守卫详解
4.1 全局前置守卫 (beforeEach)
javascript
router.beforeEach((to, from, next) => {
// 1. 检查路由是否需要认证
if (to.matched.some(record => record.meta.requiresAuth)) {
// 2. 检查用户是否已登录
if (!store.getters.isLoggedIn) {
next({
path: '/login',
query: { redirect: to.fullPath }
})
} else {
// 3. 检查用户权限
if (hasPermission(to.meta.requiredPermissions)) {
next()
} else {
next('/403') // 无权限页面
}
}
} else {
next() // 不需要认证的路由
}
})
4.2 全局解析守卫 (beforeResolve)
javascript
router.beforeResolve((to, from, next) => {
// 在所有组件内守卫和异步路由组件被解析之后调用
if (to.meta.requiresAsyncData) {
loadAsyncData().then(() => next())
} else {
next()
}
})
4.3 全局后置钩子 (afterEach)
javascript
router.afterEach((to, from) => {
// 修改页面标题
document.title = to.meta.title || '默认标题'
// 发送页面访问统计
analytics.trackPageView(to.path)
})
4.4 路由独享守卫
javascript
const routes = [
{
path: '/admin',
component: Admin,
beforeEnter: (to, from, next) => {
if (isAdminTimeWindow()) {
next()
} else {
next('/maintenance')
}
}
}
]
4.5 组件内守卫
javascript
export default {
beforeRouteEnter(to, from, next) {
// 在渲染该组件的对应路由被验证前调用
next(vm => {
// 通过 `vm` 访问组件实例
vm.loadUserData()
})
},
beforeRouteUpdate(to, from, next) {
// 在当前路由改变,但是该组件被复用时调用
this.userData = null
this.loadUserData()
next()
},
beforeRouteLeave(to, from, next) {
// 导航离开该组件的对应路由时调用
if (this.hasUnsavedChanges) {
if (confirm('您有未保存的更改,确定要离开吗?')) {
next()
} else {
next(false)
}
} else {
next()
}
}
}
5. 动态路由实现方案
5.1 方案一:基于用户角色的动态路由
javascript
// 路由配置表
const routeConfigs = {
admin: [
{ path: '/dashboard', component: Dashboard },
{ path: '/user-management', component: UserManagement },
{ path: '/system-settings', component: SystemSettings }
],
editor: [
{ path: '/dashboard', component: Dashboard },
{ path: '/content-management', component: ContentManagement }
],
viewer: [
{ path: '/dashboard', component: Dashboard }
]
}
// 动态添加路由
export function generateRoutes(userRole) {
const routes = routeConfigs[userRole] || routeConfigs.viewer
routes.forEach(route => {
router.addRoute(route)
})
// 添加404路由捕获
router.addRoute({ path: '*', redirect: '/404' })
}
5.2 方案二:基于权限码的动态路由
javascript
// 后端返回的权限列表
const userPermissions = ['user:read', 'user:write', 'content:edit']
// 权限路由映射
const permissionRouteMap = {
'user:read': { path: '/users', component: UserList },
'user:write': { path: '/users/edit', component: UserEdit },
'content:edit': { path: '/content', component: ContentManagement }
}
function buildDynamicRoutes(permissions) {
permissions.forEach(permission => {
const routeConfig = permissionRouteMap[permission]
if (routeConfig) {
router.addRoute(routeConfig)
}
})
}
5.3 方案三:服务端控制路由
javascript
// 从后端API获取路由配置
async function fetchRoutesFromServer() {
try {
const response = await api.get('/user/routes')
const routes = formatRoutes(response.data)
routes.forEach(route => {
router.addRoute(route)
})
return true
} catch (error) {
console.error('Failed to fetch routes:', error)
return false
}
}
// 格式化后端返回的路由数据
function formatRoutes(serverRoutes) {
return serverRoutes.map(route => ({
path: route.path,
name: route.name,
component: () => import(`@/views/${route.component}.vue`),
meta: route.meta
}))
}
6. 完整的权限控制系统实现
6.1 权限存储管理
javascript
// store/modules/auth.js
const state = {
token: localStorage.getItem('token'),
user: null,
permissions: [],
roles: []
}
const mutations = {
SET_TOKEN: (state, token) => {
state.token = token
localStorage.setItem('token', token)
},
SET_USER: (state, user) => {
state.user = user
},
SET_PERMISSIONS: (state, permissions) => {
state.permissions = permissions
},
SET_ROLES: (state, roles) => {
state.roles = roles
},
CLEAR_AUTH: (state) => {
state.token = ''
state.user = null
state.permissions = []
state.roles = []
localStorage.removeItem('token')
}
}
const actions = {
login({ commit }, userInfo) {
return new Promise((resolve, reject) => {
login(userInfo).then(response => {
const { token } = response.data
commit('SET_TOKEN', token)
resolve()
}).catch(error => {
reject(error)
})
})
},
getInfo({ commit, state }) {
return new Promise((resolve, reject) => {
getInfo(state.token).then(response => {
const { user, permissions, roles } = response.data
commit('SET_USER', user)
commit('SET_PERMISSIONS', permissions)
commit('SET_ROLES', roles)
resolve(response)
}).catch(error => {
reject(error)
})
})
},
logout({ commit }) {
return new Promise((resolve) => {
commit('CLEAR_AUTH')
resolve()
})
}
}
export default {
namespaced: true,
state,
mutations,
actions
}
6.2 权限检查工具
javascript
// utils/permission.js
/**
* 检查用户是否拥有指定权限
* @param {Array} requiredPerms - 需要的权限数组
* @param {Array} userPerms - 用户拥有的权限数组
* @returns {boolean}
*/
export function hasPermission(requiredPerms, userPerms) {
if (!requiredPerms || requiredPerms.length === 0) {
return true
}
if (!userPerms || userPerms.length === 0) {
return false
}
return requiredPerms.some(perm => userPerms.includes(perm))
}
/**
* 检查用户是否拥有指定角色
* @param {Array} requiredRoles - 需要的角色数组
* @param {Array} userRoles - 用户拥有的角色数组
* @returns {boolean}
*/
export function hasRole(requiredRoles, userRoles) {
if (!requiredRoles || requiredRoles.length === 0) {
return true
}
if (!userRoles || userRoles.length === 0) {
return false
}
return requiredRoles.some(role => userRoles.includes(role))
}
/**
* 深度权限检查,支持权限继承
* @param {Object} route - 路由对象
* @param {Object} user - 用户信息
* @returns {boolean}
*/
export function checkRouteAccess(route, user) {
const { meta } = route
const { permissions: userPerms, roles: userRoles } = user
// 检查角色权限
if (meta.roles && meta.roles.length > 0) {
if (!hasRole(meta.roles, userRoles)) {
return false
}
}
// 检查具体权限
if (meta.permissions && meta.permissions.length > 0) {
if (!hasPermission(meta.permissions, userPerms)) {
return false
}
}
return true
}
6.3 路由配置优化
javascript
// router/index.js
import Vue from 'vue'
import Router from 'vue-router'
import store from '@/store'
Vue.use(Router)
// 静态路由(所有用户都可访问)
const constantRoutes = [
{
path: '/login',
component: () => import('@/views/Login.vue'),
hidden: true,
meta: { public: true }
},
{
path: '/404',
component: () => import('@/views/404.vue'),
hidden: true,
meta: { public: true }
},
{
path: '/403',
component: () => import('@/views/403.vue'),
hidden: true,
meta: { public: true }
}
]
// 异步路由(根据权限动态加载)
const asyncRoutes = [
{
path: '/dashboard',
component: Layout,
redirect: '/dashboard/index',
meta: { title: '仪表盘', icon: 'dashboard' },
children: [
{
path: 'index',
component: () => import('@/views/dashboard/index.vue'),
name: 'Dashboard',
meta: { title: '首页', affix: true }
}
]
},
{
path: '/system',
component: Layout,
redirect: '/system/user',
meta: { title: '系统管理', icon: 'system', roles: ['admin'] },
children: [
{
path: 'user',
component: () => import('@/views/system/user.vue'),
name: 'UserManagement',
meta: { title: '用户管理', permissions: ['user:read'] }
},
{
path: 'role',
component: () => import('@/views/system/role.vue'),
name: 'RoleManagement',
meta: { title: '角色管理', permissions: ['role:read'] }
}
]
}
]
const createRouter = () => new Router({
mode: 'history',
scrollBehavior: () => ({ y: 0 }),
routes: constantRoutes
})
const router = createRouter()
// 重置路由(用于退出登录时)
export function resetRouter() {
const newRouter = createRouter()
router.matcher = newRouter.matcher
}
// 权限路由过滤
export function filterAsyncRoutes(routes, roles, permissions) {
const res = []
routes.forEach(route => {
const tmp = { ...route }
if (hasPermission(tmp.meta?.permissions, permissions) &&
hasRole(tmp.meta?.roles, roles)) {
if (tmp.children) {
tmp.children = filterAsyncRoutes(tmp.children, roles, permissions)
if (tmp.children.length > 0) {
res.push(tmp)
}
} else {
res.push(tmp)
}
}
})
return res
}
// 动态添加路由
export function addRoutes(routes) {
routes.forEach(route => {
router.addRoute(route)
})
// 确保404路由在最后
router.addRoute({ path: '*', redirect: '/404', hidden: true })
}
export default router
6.4 完整的权限守卫
javascript
// router/permission.js
import router from './index'
import store from '@/store'
import { Message } from 'element-ui'
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'
NProgress.configure({ showSpinner: false })
const whiteList = ['/login', '/auth-redirect'] // 白名单
router.beforeEach(async (to, from, next) => {
NProgress.start()
// 确定用户是否已登录
const hasToken = store.getters.token
if (hasToken) {
if (to.path === '/login') {
// 如果已登录,重定向到首页
next({ path: '/' })
NProgress.done()
} else {
// 检查用户信息是否已获取
const hasRoles = store.getters.roles && store.getters.roles.length > 0
if (hasRoles) {
next()
} else {
try {
// 获取用户信息
const { roles, permissions } = await store.dispatch('user/getInfo')
// 根据角色生成可访问的路由表
const accessRoutes = await store.dispatch('permission/generateRoutes', {
roles,
permissions
})
// 动态添加可访问路由
router.addRoutes(accessRoutes)
// 确保addRoutes完成后进行路由跳转
next({ ...to, replace: true })
} catch (error) {
// 获取用户信息失败,重置token并跳转到登录页
await store.dispatch('user/resetToken')
Message.error(error || 'Has Error')
next(`/login?redirect=${to.path}`)
NProgress.done()
}
}
}
} else {
/* 没有token */
if (whiteList.indexOf(to.path) !== -1) {
// 在白名单中,直接进入
next()
} else {
// 其他没有访问权限的页面被重定向到登录页面
next(`/login?redirect=${to.path}`)
NProgress.done()
}
}
})
router.afterEach(() => {
NProgress.done()
})
7. 菜单权限控制
7.1 动态菜单生成
vue
<template>
<el-menu
:default-active="$route.path"
:collapse="isCollapse"
background-color="#304156"
text-color="#bfcbd9"
active-text-color="#409EFF"
router
>
<sidebar-item
v-for="route in permission_routes"
:key="route.path"
:item="route"
:base-path="route.path"
/>
</el-menu>
</template>
<script>
import { mapGetters } from 'vuex'
import SidebarItem from './SidebarItem'
export default {
components: { SidebarItem },
computed: {
...mapGetters(['permission_routes', 'sidebar']),
isCollapse() {
return !this.sidebar.opened
}
}
}
</script>
7.2 递归菜单组件
vue
<template>
<div v-if="!item.hidden">
<template v-if="hasOneShowingChild(item.children, item) &&
(!onlyOneChild.children || onlyOneChild.noShowingChildren)">
<app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path)">
<el-menu-item :index="resolvePath(onlyOneChild.path)">
<item
:icon="onlyOneChild.meta.icon || (item.meta && item.meta.icon)"
:title="onlyOneChild.meta.title"
/>
</el-menu-item>
</app-link>
</template>
<el-submenu v-else :index="resolvePath(item.path)">
<template slot="title">
<item
v-if="item.meta"
:icon="item.meta && item.meta.icon"
:title="item.meta.title"
/>
</template>
<sidebar-item
v-for="child in item.children"
:key="child.path"
:item="child"
:base-path="resolvePath(child.path)"
/>
</el-submenu>
</div>
</template>
<script>
import path from 'path'
import { isExternal } from '@/utils/validate'
import Item from './Item'
import AppLink from './Link'
export default {
name: 'SidebarItem',
components: { Item, AppLink },
props: {
item: {
type: Object,
required: true
},
basePath: {
type: String,
default: ''
}
},
data() {
return {
onlyOneChild: null
}
},
methods: {
hasOneShowingChild(children = [], parent) {
const showingChildren = children.filter(item => {
if (item.hidden) {
return false
} else {
// 临时设置
this.onlyOneChild = item
return true
}
})
// 当只有一个子路由时,默认显示该子路由
if (showingChildren.length === 1) {
return true
}
// 如果没有子路由则显示父路由
if (showingChildren.length === 0) {
this.onlyOneChild = { ...parent, path: '', noShowingChildren: true }
return true
}
return false
},
resolvePath(routePath) {
if (isExternal(routePath)) {
return routePath
}
return path.resolve(this.basePath, routePath)
}
}
}
</script>
8. 按钮级别权限控制
8.1 权限指令实现
javascript
// directives/permission.js
import store from '@/store'
function checkPermission(el, binding) {
const { value } = binding
const permissions = store.getters && store.getters.permissions
if (value && value instanceof Array) {
if (value.length > 0) {
const permissionValues = value
const hasPermission = permissions.some(permission => {
return permissionValues.includes(permission)
})
if (!hasPermission) {
el.parentNode && el.parentNode.removeChild(el)
}
}
} else {
throw new Error(`需要权限! 例如: v-permission="['admin','editor']"`)
}
}
export default {
inserted(el, binding) {
checkPermission(el, binding)
},
update(el, binding) {
checkPermission(el, binding)
}
}
8.2 全局注册指令
javascript
// main.js
import permission from '@/directives/permission'
Vue.directive('permission', permission)
8.3 使用方法
vue
<template>
<div>
<!-- 需要admin或editor权限 -->
<el-button v-permission="['admin', 'editor']">编辑</el-button>
<!-- 需要delete权限 -->
<el-button
v-permission="['user:delete']"
type="danger"
>
删除
</el-button>
<!-- 权限判断方法 -->
<el-button v-if="hasPermission(['admin'])" type="primary">
管理员操作
</el-button>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
computed: {
...mapGetters(['permissions'])
},
methods: {
hasPermission(requiredPermissions) {
return requiredPermissions.some(perm =>
this.permissions.includes(perm)
)
}
}
}
</script>
9. 高级权限功能
9.1 数据权限控制
javascript
// mixins/dataPermission.js
export default {
methods: {
// 数据级别权限检查
checkDataPermission(resource, action, data) {
const userPermissions = this.$store.getters.permissions
const user = this.$store.getters.user
// 基于资源类型的权限
const resourcePermission = `${resource}:${action}`
if (!userPermissions.includes(resourcePermission)) {
return false
}
// 基于数据属性的权限
if (data && data.department !== user.department &&
!userPermissions.includes('cross_department:access')) {
return false
}
return true
},
// 过滤数据基于权限
filterDataByPermission(dataList, resource, action) {
return dataList.filter(item =>
this.checkDataPermission(resource, action, item)
)
}
}
}
9.2 权限变更监听
javascript
// utils/permissionWatcher.js
export class PermissionWatcher {
constructor(store) {
this.store = store
this.listeners = new Set()
this.watchStore()
}
watchStore() {
this.store.watch(
state => state.user.permissions,
(newPerms, oldPerms) => {
this.onPermissionsChange(newPerms, oldPerms)
},
{ deep: true }
)
}
onPermissionsChange(newPerms, oldPerms) {
this.listeners.forEach(listener => {
listener(newPerms, oldPerms)
})
}
subscribe(listener) {
this.listeners.add(listener)
return () => this.listeners.delete(listener)
}
// 检查权限变更影响
checkRouteAccessibilityChange(newPerms, oldPerms, routes) {
const affectedRoutes = routes.filter(route => {
const hadAccess = this.checkAccess(route, oldPerms)
const hasAccess = this.checkAccess(route, newPerms)
return hadAccess !== hasAccess
})
return affectedRoutes
}
checkAccess(route, permissions) {
const requiredPerms = route.meta?.permissions || []
return requiredPerms.length === 0 ||
requiredPerms.some(perm => permissions.includes(perm))
}
}
10. 性能优化与最佳实践
10.1 路由懒加载优化
javascript
// 使用webpack魔法注释优化chunk
const routes = [
{
path: '/user',
component: () => import(/* webpackChunkName: "user" */ '@/views/user/index.vue')
},
{
path: '/settings',
component: () => import(/* webpackChunkName: "settings" */ '@/views/settings/index.vue')
}
]
10.2 权限缓存策略
javascript
// utils/permissionCache.js
class PermissionCache {
constructor() {
this.cache = new Map()
this.ttl = 5 * 60 * 1000 // 5分钟
}
getKey(permissions, route) {
return `${permissions.join(',')}-${route.path}`
}
get(permissions, route) {
const key = this.getKey(permissions, route)
const item = this.cache.get(key)
if (item && Date.now() - item.timestamp < this.ttl) {
return item.result
}
return null
}
set(permissions, route, result) {
const key = this.getKey(permissions, route)
this.cache.set(key, {
result,
timestamp: Date.now()
})
}
clear() {
this.cache.clear()
}
}
export const permissionCache = new PermissionCache()
10.3 错误处理与降级方案
javascript
// router/errorHandler.js
export class RouterErrorHandler {
static handleNavigationError(error) {
// 路由重复导航错误
if (error.name === 'NavigationDuplicated') {
return
}
// 权限错误
if (error.message.includes('permission')) {
this.handlePermissionError(error)
return
}
// 路由不存在
if (error.message.includes('route')) {
this.handleRouteNotFound(error)
return
}
console.error('路由错误:', error)
}
static handlePermissionError(error) {
// 跳转到无权限页面
if (router.currentRoute.path !== '/403') {
router.replace('/403')
}
}
static handleRouteNotFound(error) {
// 跳转到404页面
if (router.currentRoute.path !== '/404') {
router.replace('/404')
}
}
}
// 注册全局错误处理
router.onError(error => {
RouterErrorHandler.handleNavigationError(error)
})
11. 测试与调试
11.1 权限测试工具
javascript
// tests/unit/permission.spec.js
import { hasPermission, hasRole } from '@/utils/permission'
describe('权限工具函数', () => {
describe('hasPermission', () => {
it('应该在没有要求权限时返回true', () => {
expect(hasPermission(null, ['user'])).toBe(true)
expect(hasPermission([], ['user'])).toBe(true)
})
it('应该在用户没有权限时返回false', () => {
expect(hasPermission(['admin'], [])).toBe(false)
expect(hasPermission(['admin'], ['user'])).toBe(false)
})
it('应该在用户有权限时返回true', () => {
expect(hasPermission(['user'], ['user'])).toBe(true)
expect(hasPermission(['user', 'admin'], ['user'])).toBe(true)
})
})
describe('hasRole', () => {
it('应该正确检查角色权限', () => {
expect(hasRole(['admin'], ['admin'])).toBe(true)
expect(hasRole(['admin'], ['user'])).toBe(false)
expect(hasRole(['admin', 'editor'], ['editor'])).toBe(true)
})
})
})
11.2 路由守卫测试
javascript
// tests/unit/router.spec.js
import { shallowMount, createLocalVue } from '@vue/test-utils'
import VueRouter from 'vue-router'
import { permission } from '@/router/permission'
const localVue = createLocalVue()
localVue.use(VueRouter)
describe('路由权限守卫', () => {
let router
let store
beforeEach(() => {
router = new VueRouter({ routes: [] })
store = {
getters: {
token: null,
roles: []
}
}
})
it('应该重定向未登录用户到登录页', async () => {
const to = { path: '/protected', matched: [{ meta: { requiresAuth: true } }] }
const from = { path: '/' }
const next = jest.fn()
await permission(to, from, next, store)
expect(next).toHaveBeenCalledWith('/login')
})
})
12. 安全考虑
12.1 前端安全最佳实践
javascript
// security/config.js
export const SecurityConfig = {
// 令牌刷新间隔
tokenRefreshInterval: 15 * 60 * 1000,
// 会话超时时间
sessionTimeout: 2 * 60 * 60 * 1000,
// 最大并发请求数
maxConcurrentRequests: 5,
// 敏感操作需要重新认证
sensitiveOperations: ['delete', 'transfer', 'reset']
}
// 安全中间件
export const securityMiddleware = (store) => (to, from, next) => {
// 检查会话超时
if (isSessionExpired(store.getters.lastActivity)) {
store.dispatch('user/logout')
next('/login?reason=timeout')
return
}
// 更新最后活动时间
store.commit('user/UPDATE_LAST_ACTIVITY')
// 检查敏感操作
if (isSensitiveOperation(to) && !store.getters.recentlyAuthenticated) {
next('/reauthenticate?redirect=' + encodeURIComponent(to.fullPath))
return
}
next()
}
function isSessionExpired(lastActivity) {
return Date.now() - lastActivity > SecurityConfig.sessionTimeout
}
function isSensitiveOperation(route) {
return SecurityConfig.sensitiveOperations.some(op =>
route.path.includes(op) || route.meta?.requiresReauth
)
}
13. 总结
Vue路由权限控制是一个综合性的系统工程,需要从前端到后端的全方位考虑。本文详细介绍了从基础概念到高级实践的完整解决方案,包括:
-
基础架构:路由守卫、动态路由、权限检查
-
完整实现:Store管理、菜单控制、按钮权限
-
高级功能:数据权限、权限监听、性能优化
-
工程实践:测试方案、安全考虑、错误处理
在实际项目中,应根据具体业务需求选择合适的权限控制方案,并始终牢记"前端权限控制是用户体验优化,后端权限控制是安全保障"的原则。
更多推荐



所有评论(0)