权限控制
vue3-element-admin 提供了完善的权限控制系统,支持多种权限控制方式。
权限类型
1. 路由权限
通过动态路由实现页面级权限控制。用户登录后,根据其角色权限动态生成可访问的路由。
实现原理:
typescript
// src/router/index.ts
// 根据用户权限过滤路由
const accessRoutes = await permissionStore.generateRoutes(roles)
accessRoutes.forEach((route) => {
router.addRoute(route)
})1
2
3
4
5
6
2
3
4
5
6
2. 按钮权限
通过指令或函数实现按钮级权限控制。
指令方式:
vue
<template>
<!-- 用户拥有指定权限才显示 -->
<el-button v-hasPerm="['sys:user:add']">新增</el-button>
<!-- 用户拥有指定角色才显示 -->
<el-button v-hasRole="['ADMIN']">删除</el-button>
</template>1
2
3
4
5
6
7
2
3
4
5
6
7
函数方式:
vue
<script setup lang="ts">
import { hasPerm, hasRole } from '@/utils/permission'
// 在逻辑中判断权限
if (hasPerm(['sys:user:add'])) {
// 执行操作
}
</script>1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
3. 角色权限
基于角色的访问控制(RBAC),用户通过角色获得权限。
配置示例:
typescript
// 路由元信息中配置角色
{
path: '/system/user',
meta: {
title: '用户管理',
roles: ['ADMIN', 'USER'] // 允许访问的角色
}
}1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
4. 数据权限
控制用户可以访问的数据范围。
实现方式:
- 全部数据权限:可以查看所有数据
- 本部门数据权限:只能查看本部门数据
- 本部门及下级部门数据权限:可以查看本部门和下级部门数据
- 仅本人数据权限:只能查看自己的数据
- 自定义数据权限:自定义可访问的部门
权限配置
路由配置
在路由的 meta 中配置权限:
typescript
// src/router/modules/system.ts
export default [
{
path: '/system',
component: Layout,
redirect: '/system/user',
meta: {
title: '系统管理',
icon: 'setting',
hidden: false,
alwaysShow: true,
roles: ['ADMIN'] // 只有 ADMIN 角色可访问
},
children: [
{
path: 'user',
component: () => import('@/views/system/user/index.vue'),
name: 'User',
meta: {
title: '用户管理',
icon: 'user',
keepAlive: true,
perms: ['sys:user:list'] // 需要的权限标识
}
}
]
}
]1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
meta 属性说明:
| 属性 | 类型 | 说明 |
|---|---|---|
title | string | 路由标题(菜单名称) |
icon | string | 菜单图标 |
hidden | boolean | 是否隐藏菜单 |
alwaysShow | boolean | 是否总是显示根菜单 |
roles | string[] | 允许访问的角色 |
perms | string[] | 需要的权限标识 |
keepAlive | boolean | 是否缓存页面 |
affix | boolean | 是否固定在 tags-view |
按钮权限配置
使用 v-hasPerm 指令:
vue
<template>
<div>
<!-- 单个权限 -->
<el-button v-hasPerm="['sys:user:add']">新增</el-button>
<!-- 多个权限(满足其一即可) -->
<el-button v-hasPerm="['sys:user:edit', 'sys:user:update']">
编辑
</el-button>
<!-- 需要同时满足多个权限 -->
<el-button v-hasPerm="['sys:user:delete']">删除</el-button>
</div>
</template>1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
使用 v-hasRole 指令:
vue
<template>
<div>
<!-- 单个角色 -->
<el-button v-hasRole="['ADMIN']">管理员操作</el-button>
<!-- 多个角色(满足其一即可) -->
<el-button v-hasRole="['ADMIN', 'USER']">用户操作</el-button>
</div>
</template>1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
权限指令实现
hasPerm 指令
typescript
// src/directives/permission/hasPerm.ts
import { useUserStore } from '@/store/modules/user'
export default {
mounted(el: HTMLElement, binding: any) {
const { value } = binding
const perms = useUserStore().perms
if (value && value instanceof Array && value.length > 0) {
const requiredPerms = value
const hasPermission = perms.some((perm: string) => {
return requiredPerms.includes(perm)
})
if (!hasPermission) {
el.parentNode && el.parentNode.removeChild(el)
}
} else {
throw new Error('需要传入权限标识数组')
}
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
hasRole 指令
typescript
// src/directives/permission/hasRole.ts
import { useUserStore } from '@/store/modules/user'
export default {
mounted(el: HTMLElement, binding: any) {
const { value } = binding
const roles = useUserStore().roles
if (value && value instanceof Array && value.length > 0) {
const requiredRoles = value
const hasRole = roles.some((role: string) => {
return requiredRoles.includes(role)
})
if (!hasRole) {
el.parentNode && el.parentNode.removeChild(el)
}
} else {
throw new Error('需要传入角色标识数组')
}
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
权限工具函数
typescript
// src/utils/permission.ts
import { useUserStore } from '@/store/modules/user'
/**
* 检查是否有权限
* @param perms 权限标识数组
* @returns 是否有权限
*/
export function hasPerm(perms: string[]): boolean {
const userStore = useUserStore()
const userPerms = userStore.perms
return userPerms.some((perm: string) => {
return perms.includes(perm)
})
}
/**
* 检查是否有角色
* @param roles 角色标识数组
* @returns 是否有角色
*/
export function hasRole(roles: string[]): boolean {
const userStore = useUserStore()
const userRoles = userStore.roles
return userRoles.some((role: string) => {
return roles.includes(role)
})
}
/**
* 检查是否有任一权限
* @param perms 权限标识数组
* @returns 是否有任一权限
*/
export function hasAnyPerm(perms: string[]): boolean {
const userStore = useUserStore()
const userPerms = userStore.perms
return perms.some((perm: string) => {
return userPerms.includes(perm)
})
}
/**
* 检查是否有所有权限
* @param perms 权限标识数组
* @returns 是否有所有权限
*/
export function hasAllPerms(perms: string[]): boolean {
const userStore = useUserStore()
const userPerms = userStore.perms
return perms.every((perm: string) => {
return userPerms.includes(perm)
})
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
权限流程
登录流程
mermaid
graph LR
A[用户登录] --> B[获取 Token]
B --> C[存储 Token]
C --> D[获取用户信息]
D --> E[获取用户权限]
E --> F[生成动态路由]
F --> G[跳转首页]1
2
3
4
5
6
7
2
3
4
5
6
7
路由守卫
typescript
// src/router/index.ts
router.beforeEach(async (to, from, next) => {
const userStore = useUserStore()
const permissionStore = usePermissionStore()
// 判断是否登录
if (userStore.token) {
if (to.path === '/login') {
// 已登录,重定向到首页
next({ path: '/' })
} else {
// 判断是否已获取用户信息
if (userStore.roles.length === 0) {
try {
// 获取用户信息
await userStore.getUserInfo()
// 生成动态路由
const accessRoutes = await permissionStore.generateRoutes(
userStore.roles
)
// 动态添加路由
accessRoutes.forEach((route) => {
router.addRoute(route)
})
// 确保添加路由完成
next({ ...to, replace: true })
} catch (error) {
// 获取用户信息失败,重新登录
await userStore.logout()
next({ path: '/login' })
}
} else {
next()
}
}
} else {
// 未登录
if (whiteList.includes(to.path)) {
// 白名单路径,直接访问
next()
} else {
// 重定向到登录页
next({ path: '/login', query: { redirect: to.path } })
}
}
})1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
动态路由生成
typescript
// src/store/modules/permission.ts
import { defineStore } from 'pinia'
export const usePermissionStore = defineStore('permission', () => {
const routes = ref<RouteRecordRaw[]>([])
/**
* 生成动态路由
* @param roles 用户角色
*/
async function generateRoutes(roles: string[]) {
// 获取后端返回的路由数据
const res = await getRoutes()
const asyncRoutes = res.data
// 过滤有权限的路由
const accessedRoutes = filterAsyncRoutes(asyncRoutes, roles)
routes.value = constantRoutes.concat(accessedRoutes)
return accessedRoutes
}
/**
* 递归过滤路由
*/
function filterAsyncRoutes(routes: any[], roles: string[]) {
const res: RouteRecordRaw[] = []
routes.forEach((route) => {
const tmp = { ...route }
if (hasPermission(roles, tmp)) {
if (tmp.children) {
tmp.children = filterAsyncRoutes(tmp.children, roles)
}
res.push(tmp)
}
})
return res
}
/**
* 判断是否有权限
*/
function hasPermission(roles: string[], route: any) {
if (route.meta && route.meta.roles) {
return roles.some((role) => route.meta.roles.includes(role))
}
return true
}
return {
routes,
generateRoutes
}
})1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
最佳实践
1. 权限标识命名规范
采用 模块:功能:操作 的格式:
sys:user:list // 用户列表查询
sys:user:add // 用户新增
sys:user:edit // 用户编辑
sys:user:delete // 用户删除
sys:user:export // 用户导出1
2
3
4
5
2
3
4
5
2. 角色命名规范
使用大写字母和下划线:
ADMIN // 管理员
USER // 普通用户
GUEST // 访客1
2
3
2
3
3. 前后端权限一致
- 前端权限标识与后端保持一致
- 前端校验 + 后端校验双重保障
- 敏感操作务必在后端进行权限校验
4. 权限缓存
- 权限信息缓存在 Pinia Store 中
- 避免频繁请求后端获取权限
- Token 过期后清除权限缓存
常见问题
1. 刷新页面后路由消失
原因:动态添加的路由没有持久化
解决:在路由守卫中重新生成路由
2. 权限更新不及时
原因:权限信息缓存未更新
解决:重新登录或清除缓存
3. 按钮权限不生效
原因:
- 权限标识配置错误
- 用户权限未正确获取
- 指令使用不当
解决:
- 检查权限标识是否正确
- 确认用户权限已正确获取
- 确认指令使用方式正确
