设计理念
1. 权限模型设计
-
RBAC (基于角色的访问控制):用户-角色-权限三级模型
-
前端路由与菜单分离:路由负责页面跳转,菜单负责导航展示
-
数据驱动:权限数据决定菜单渲染
-
分层控制:页面级、模块级、操作级权限控制
2. 核心原则
-
安全性:前端权限验证是用户体验优化,核心验证应在后端
-
可维护性:权限配置集中管理,易于修改和扩展
-
用户体验:无权限内容对用户完全隐藏
-
性能:按需加载权限相关资源
完整实现示例
1. 项目结构
text
src/
├── components/
│ ├── Layout/
│ │ ├── AppLayout.vue
│ │ └── Sidebar.vue
│ └── common/
├── router/
│ ├── index.js
│ └── routes.js
├── store/
│ ├── index.js
│ ├── modules/
│ │ └── user.js
│ └── types.js
├── utils/
│ └── permission.js
├── api/
│ └── user.js
└── views/
├── dashboard/
├── user/
├── settings/
└── 404.vue
2. 权限数据模型
javascript
// store/types.js
export const USER_SET_ROLES = 'USER_SET_ROLES'
export const USER_SET_PERMISSIONS = 'USER_SET_PERMISSIONS'
export const USER_SET_MENUS = 'USER_SET_MENUS'
// 权限类型枚举
export const PERMISSION_TYPES = {
MENU: 'menu',
BUTTON: 'button',
API: 'api'
}
3. 路由配置
javascript
// router/routes.js
// 静态路由 - 所有用户都可访问
export const constantRoutes = [
{
path: '/login',
component: () => import('@/views/Login.vue'),
hidden: true
},
{
path: '/404',
component: () => import('@/views/404.vue'),
hidden: true
}
]
// 动态路由 - 根据权限动态添加
export const asyncRoutes = [
{
path: '/dashboard',
component: () => import('@/views/dashboard/Index.vue'),
name: 'Dashboard',
meta: {
title: '仪表盘',
icon: 'dashboard',
permissions: ['dashboard:view']
}
},
{
path: '/user',
component: () => import('@/views/user/Index.vue'),
redirect: '/user/list',
name: 'User',
meta: {
title: '用户管理',
icon: 'user',
permissions: ['user:view']
},
children: [
{
path: 'list',
component: () => import('@/views/user/List.vue'),
name: 'UserList',
meta: {
title: '用户列表',
permissions: ['user:list']
}
},
{
path: 'create',
component: () => import('@/views/user/Create.vue'),
name: 'UserCreate',
meta: {
title: '新增用户',
permissions: ['user:create']
}
},
{
path: 'edit/:id',
component: () => import('@/views/user/Edit.vue'),
name: 'UserEdit',
meta: {
title: '编辑用户',
permissions: ['user:edit'],
hidden: true // 不在菜单显示
}
}
]
},
{
path: '/system',
component: () => import('@/views/system/Index.vue'),
redirect: '/system/role',
name: 'System',
meta: {
title: '系统管理',
icon: 'system',
permissions: ['system:view']
},
children: [
{
path: 'role',
component: () => import('@/views/system/Role.vue'),
name: 'Role',
meta: {
title: '角色管理',
permissions: ['role:view']
}
},
{
path: 'permission',
component: () => import('@/views/system/Permission.vue'),
name: 'Permission',
meta: {
title: '权限管理',
permissions: ['permission:view']
}
}
]
},
// 404页面必须放在最后
{ path: '*', redirect: '/404', hidden: true }
]
4. Vuex状态管理
javascript
// store/modules/user.js
import { constantRoutes, asyncRoutes } from '@/router/routes'
import { USER_SET_ROLES, USER_SET_PERMISSIONS, USER_SET_MENUS } from '../types'
const state = {
roles: [],
permissions: [],
menus: [],
routes: constantRoutes // 初始只有静态路由
}
const mutations = {
[USER_SET_ROLES](state, roles) {
state.roles = roles
},
[USER_SET_PERMISSIONS](state, permissions) {
state.permissions = permissions
},
[USER_SET_MENUS](state, menus) {
state.menus = menus
},
SET_ROUTES(state, routes) {
state.routes = constantRoutes.concat(routes)
}
}
const actions = {
// 生成用户权限路由
generateRoutes({ commit, state }, permissions) {
return new Promise(resolve => {
const accessedRoutes = filterAsyncRoutes(asyncRoutes, permissions)
commit('SET_ROUTES', accessedRoutes)
resolve(accessedRoutes)
})
},
// 获取用户信息
async getUserInfo({ commit, dispatch }) {
try {
// 模拟API调用
const userInfo = await getUserInfoAPI()
const { roles, permissions } = userInfo
commit(USER_SET_ROLES, roles)
commit(USER_SET_PERMISSIONS, permissions)
// 根据权限生成路由
const accessedRoutes = await dispatch('generateRoutes', permissions)
// 生成菜单(过滤掉hidden为true的路由)
const menus = generateMenus(accessedRoutes)
commit(USER_SET_MENUS, menus)
return {
roles,
permissions,
menus
}
} catch (error) {
console.error('获取用户信息失败:', error)
throw error
}
}
}
// 工具函数:根据权限过滤路由
function filterAsyncRoutes(routes, permissions) {
const res = []
routes.forEach(route => {
const tmp = { ...route }
// 检查路由权限
if (hasPermission(permissions, tmp)) {
if (tmp.children) {
tmp.children = filterAsyncRoutes(tmp.children, permissions)
// 如果过滤后子路由不为空,才保留父路由
if (tmp.children.length > 0) {
res.push(tmp)
}
} else {
res.push(tmp)
}
}
})
return res
}
// 检查是否有权限
function hasPermission(permissions, route) {
if (route.meta && route.meta.permissions) {
return permissions.some(permission =>
route.meta.permissions.includes(permission)
)
}
return true // 没有设置权限要求的路由默认允许访问
}
// 生成菜单
function generateMenus(routes) {
const menus = []
routes.forEach(route => {
// 跳过隐藏的路由
if (route.hidden) return
const menu = {
path: route.path,
name: route.name,
meta: { ...route.meta },
children: []
}
if (route.children && route.children.length > 0) {
menu.children = generateMenus(route.children)
// 如果子菜单为空,则不显示父菜单
if (menu.children.length === 0) return
}
menus.push(menu)
})
return menus
}
export default {
namespaced: true,
state,
mutations,
actions
}
5. 权限工具函数
javascript
// utils/permission.js
import store from '@/store'
/**
* 检查是否有权限
* @param {Array} needPermissions 需要的权限
* @returns {Boolean}
*/
export function hasPermission(needPermissions) {
if (!needPermissions || needPermissions.length === 0) {
return true
}
const userPermissions = store.getters.permissions
return userPermissions.some(permission =>
needPermissions.includes(permission)
)
}
/**
* 检查是否有角色
* @param {Array} needRoles 需要的角色
* @returns {Boolean}
*/
export function hasRole(needRoles) {
if (!needRoles || needRoles.length === 0) {
return true
}
const userRoles = store.getters.roles
return userRoles.some(role => needRoles.includes(role))
}
// 权限指令
export const permissionDirective = {
inserted(el, binding) {
const { value } = binding
const permissions = store.getters.permissions
if (value && value instanceof Array && value.length > 0) {
const hasPerm = permissions.some(permission =>
value.includes(permission)
)
if (!hasPerm) {
el.parentNode && el.parentNode.removeChild(el)
}
} else {
throw new Error(`需要权限数组! 如: v-permission="['user:create']"`)
}
}
}
// 角色指令
export const roleDirective = {
inserted(el, binding) {
const { value } = binding
const roles = store.getters.roles
if (value && value instanceof Array && value.length > 0) {
const hasRole = roles.some(role => value.includes(role))
if (!hasRole) {
el.parentNode && el.parentNode.removeChild(el)
}
} else {
throw new Error(`需要角色数组! 如: v-role="['admin']"`)
}
}
}
6. 侧边栏菜单组件
vue
<!-- components/Layout/Sidebar.vue -->
<template>
<el-scrollbar wrap-class="scrollbar-wrapper">
<el-menu
:default-active="activeMenu"
:collapse="isCollapse"
:background-color="variables.menuBg"
:text-color="variables.menuText"
:active-text-color="variables.menuActiveText"
:unique-opened="false"
:collapse-transition="false"
mode="vertical"
>
<sidebar-item
v-for="route in menus"
:key="route.path"
:item="route"
:base-path="route.path"
/>
</el-menu>
</el-scrollbar>
</template>
<script>
import { mapGetters } from 'vuex'
import SidebarItem from './SidebarItem.vue'
import variables from '@/styles/variables.scss'
export default {
name: 'Sidebar',
components: { SidebarItem },
computed: {
...mapGetters(['menus']),
activeMenu() {
const route = this.$route
const { meta, path } = route
if (meta.activeMenu) {
return meta.activeMenu
}
return path
},
isCollapse() {
return !this.$store.state.app.sidebar.opened
},
variables() {
return variables
}
}
}
</script>
7. 菜单项组件
vue
<!-- components/Layout/SidebarItem.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)" :class="{'submenu-title-noDropdown':!isNest}">
<item :icon="onlyOneChild.meta.icon || (item.meta && item.meta.icon)" :title="onlyOneChild.meta.title" />
</el-menu-item>
</app-link>
</template>
<el-submenu v-else ref="subMenu" :index="resolvePath(item.path)" popper-append-to-body>
<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"
:is-nest="true"
:item="child"
:base-path="resolvePath(child.path)"
class="nest-menu"
/>
</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
},
isNest: {
type: Boolean,
default: false
},
basePath: {
type: String,
default: ''
}
},
data() {
this.onlyOneChild = null
return {}
},
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
}
if (isExternal(this.basePath)) {
return this.basePath
}
return path.resolve(this.basePath, routePath)
}
}
}
</script>
8. 权限指令注册
javascript
// main.js
import Vue from 'vue'
import { permissionDirective, roleDirective } from '@/utils/permission'
// 注册权限指令
Vue.directive('permission', permissionDirective)
Vue.directive('role', roleDirective)
9. 路由守卫
javascript
// router/index.js
import Vue from 'vue'
import Router from 'vue-router'
import { constantRoutes } from './routes'
import store from '@/store'
import { Message } from 'element-ui'
Vue.use(Router)
const router = new Router({
mode: 'history',
scrollBehavior: () => ({ y: 0 }),
routes: constantRoutes
})
// 白名单
const whiteList = ['/login', '/auth-redirect']
router.beforeEach(async (to, from, next) => {
// 设置页面标题
if (to.meta.title) {
document.title = to.meta.title
}
// 判断是否有token
const hasToken = store.getters.token
if (hasToken) {
if (to.path === '/login') {
next({ path: '/' })
} else {
const hasRoles = store.getters.roles && store.getters.roles.length > 0
if (hasRoles) {
next()
} else {
try {
// 获取用户信息
const { roles } = await store.dispatch('user/getUserInfo')
// 根据角色生成可访问路由
const accessRoutes = await store.dispatch('user/generateRoutes', roles)
// 动态添加路由
router.addRoutes(accessRoutes)
// 确保addRoutes完成后next
next({ ...to, replace: true })
} catch (error) {
// 失败则重置token并跳转到登录页
await store.dispatch('user/resetToken')
Message.error(error || 'Has Error')
next(`/login?redirect=${to.path}`)
}
}
}
} else {
// 没有token
if (whiteList.indexOf(to.path) !== -1) {
next()
} else {
next(`/login?redirect=${to.path}`)
}
}
})
router.afterEach(() => {
// 完成进度条
})
export default router
10. 在页面中使用权限控制
vue
<!-- views/user/List.vue -->
<template>
<div class="user-list">
<div class="header">
<el-button
v-permission="['user:create']"
type="primary"
@click="handleCreate"
>
新增用户
</el-button>
<el-button
v-permission="['user:export']"
@click="handleExport"
>
导出数据
</el-button>
</div>
<el-table :data="userList">
<el-table-column prop="name" label="姓名" />
<el-table-column prop="email" label="邮箱" />
<el-table-column prop="role" label="角色" />
<el-table-column label="操作">
<template slot-scope="scope">
<el-button
v-permission="['user:edit']"
size="mini"
@click="handleEdit(scope.row)"
>
编辑
</el-button>
<el-button
v-permission="['user:delete']"
size="mini"
type="danger"
@click="handleDelete(scope.row)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script>
export default {
name: 'UserList',
data() {
return {
userList: []
}
},
methods: {
handleCreate() {
// 新增用户逻辑
},
handleEdit(user) {
// 编辑用户逻辑
},
handleDelete(user) {
// 删除用户逻辑
},
handleExport() {
// 导出数据逻辑
}
}
}
</script>
设计要点说明
1. 权限粒度控制
-
页面级:通过路由守卫控制
-
菜单级:通过动态菜单生成控制
-
操作级:通过指令控制按钮显示
2. 性能优化
-
路由懒加载
-
按需生成权限路由
-
菜单数据缓存
3. 安全性
-
前端权限验证是用户体验优化
-
关键接口必须在后端验证权限
-
敏感操作需要二次确认
4. 扩展性
-
支持多级嵌套路由
-
支持多种权限验证方式
-
易于集成新的权限类型
这个权限管理系统提供了完整的解决方案,从路由控制到按钮级别的权限管理,具有良好的可维护性和扩展性。
691

被折叠的 条评论
为什么被折叠?



