权限管理
CmdAdmin 实现了完整的 RBAC(基于角色的访问控制)权限体系,支持三级权限控制。
权限体系架构
┌─────────────────────────────────────────────────────────────┐
│ 权限体系 │
├─────────────┬─────────────┬─────────────────────────────────┤
│ 菜单级权限 │ 按钮级权限 │ 数据级权限 │
├─────────────┼─────────────┼─────────────────────────────────┤
│ 页面访问 │ 操作按钮 │ 数据范围(部门隔离) │
│ 路由控制 │ 功能接口 │ 数据过滤 │
│ 侧边栏菜单 │ 批量操作 │ 敏感数据保护 │
└─────────────┴─────────────┴─────────────────────────────────┘菜单级权限
动态路由生成
系统根据用户权限动态生成可访问的路由:
java
@GetMapping("/auth/routers")
public Result<List<RouterVO>> getRouters() {
Long userId = SecurityUtils.getCurrentUserId();
// 获取用户有权限的菜单
List<SysMenu> menus = menuService.selectMenuTreeByUserId(userId);
// 构建路由树
List<RouterVO> routers = buildRouterTree(menus);
return Result.ok(routers);
}前端路由注册
typescript
// stores/auth.ts
const registerRoutes = (menus: Menu[]) => {
const modules = import.meta.glob('@/views/**/*.vue')
menus.forEach(menu => {
const route: RouteRecordRaw = {
path: menu.path,
name: menu.name,
component: modules[`/src/views/${menu.component}.vue`],
meta: {
title: menu.title,
icon: menu.icon,
permissions: menu.permissions
}
}
router.addRoute(route)
})
}按钮级权限
v-permission 指令
vue
<template>
<!-- 有权限时显示,无权限时移除元素 -->
<el-button v-permission="'system:user:add'">新增用户</el-button>
<!-- 有权限时显示,无权限时禁用 -->
<el-button v-permission:disabled="'system:user:edit'">编辑</el-button>
<!-- 有权限时显示,无权限时隐藏 -->
<el-button v-permission.hide="'system:user:delete'">删除</el-button>
</template>权限标识规范
权限标识格式:模块:功能:操作
| 权限标识 | 说明 |
|---|---|
system:user:list | 查看用户列表 |
system:user:add | 新增用户 |
system:user:edit | 编辑用户 |
system:user:delete | 删除用户 |
system:user:export | 导出用户 |
system:role:assign | 分配角色权限 |
后端接口权限
java
@RestController
@RequestMapping("/system/user")
public class SysUserController {
@PreAuthorize("hasAuthority('system:user:list')")
@GetMapping("/list")
public Result<PageResult<User>> list(UserQuery query) {
// 查询用户列表
}
@PreAuthorize("hasAuthority('system:user:add')")
@PostMapping
public Result<Void> add(@RequestBody @Validated User user) {
// 新增用户
}
@PreAuthorize("hasAuthority('system:user:edit')")
@PutMapping
public Result<Void> edit(@RequestBody @Validated User user) {
// 编辑用户
}
@PreAuthorize("hasAuthority('system:user:delete')")
@DeleteMapping("/{id}")
public Result<Void> delete(@PathVariable Long id) {
// 删除用户
}
}数据级权限
数据范围控制
java
@DataScope(deptAlias = "d", userAlias = "u")
@PreAuthorize("hasAuthority('system:user:list')")
@GetMapping("/list")
public Result<PageResult<User>> list(UserQuery query) {
// 自动根据数据权限过滤数据
return userService.selectUserList(query);
}数据权限注解
java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DataScope {
/**
* 部门表的别名
*/
String deptAlias() default "";
/**
* 用户表的别名
*/
String userAlias() default "";
}数据权限切面
java
@Aspect
@Component
public class DataScopeAspect {
@Before("@annotation(dataScope)")
public void doBefore(JoinPoint point, DataScope dataScope) {
// 获取当前用户
LoginUser user = SecurityUtils.getCurrentUser();
// 如果是超级管理员,不限制数据范围
if (user.isAdmin()) {
return;
}
// 根据用户的数据权限范围构建 SQL 过滤条件
StringBuilder sql = new StringBuilder();
switch (user.getDataScope()) {
case "1": // 全部数据权限
break;
case "2": // 本部门数据权限
sql.append(String.format(" AND %s.dept_id = %d",
dataScope.deptAlias(), user.getDeptId()));
break;
case "3": // 本部门及以下数据权限
sql.append(String.format(" AND %s.dept_id IN (%s)",
dataScope.deptAlias(),
getChildDeptIds(user.getDeptId())));
break;
case "4": // 仅本人数据权限
sql.append(String.format(" AND %s.user_id = %d",
dataScope.userAlias(), user.getUserId()));
break;
case "5": // 自定义数据权限
sql.append(String.format(" AND %s.dept_id IN (%s)",
dataScope.deptAlias(),
getCustomDeptIds(user.getRoleIds())));
break;
}
// 将 SQL 条件存入线程变量
DataScopeContext.set(sql.toString());
}
}权限数据模型
数据库表结构
sql
-- 用户表
CREATE TABLE sys_user (
id BIGSERIAL PRIMARY KEY,
username VARCHAR(50) NOT NULL,
password VARCHAR(100) NOT NULL,
dept_id BIGINT,
-- ...
);
-- 角色表
CREATE TABLE sys_role (
id BIGSERIAL PRIMARY KEY,
role_name VARCHAR(50) NOT NULL,
role_key VARCHAR(50) NOT NULL,
data_scope VARCHAR(1) DEFAULT '1', -- 数据范围(1全部 2本部门 3本部门及以下 4本人 5自定义)
-- ...
);
-- 菜单表
CREATE TABLE sys_menu (
id BIGSERIAL PRIMARY KEY,
menu_name VARCHAR(50) NOT NULL,
permission VARCHAR(100), -- 权限标识
menu_type VARCHAR(1), -- 菜单类型(M目录 C菜单 F按钮)
-- ...
);
-- 用户角色关联表
CREATE TABLE sys_user_role (
user_id BIGINT NOT NULL,
role_id BIGINT NOT NULL,
PRIMARY KEY (user_id, role_id)
);
-- 角色菜单关联表
CREATE TABLE sys_role_menu (
role_id BIGINT NOT NULL,
menu_id BIGINT NOT NULL,
PRIMARY KEY (role_id, menu_id)
);权限分配流程
1. 创建角色
java
@PostMapping
@PreAuthorize("hasAuthority('system:role:add')")
public Result<Void> add(@RequestBody @Validated SysRole role) {
// 保存角色
roleService.save(role);
return Result.ok();
}2. 分配菜单权限
java
@PutMapping("/menu")
@PreAuthorize("hasAuthority('system:role:menu:assign')")
public Result<Void> assignMenu(@RequestBody RoleMenuDTO dto) {
roleService.assignMenu(dto.getRoleId(), dto.getMenuIds());
return Result.ok();
}3. 分配用户角色
java
@PutMapping("/user")
@PreAuthorize("hasAuthority('system:user:role:assign')")
public Result<Void> assignRole(@RequestBody UserRoleDTO dto) {
userService.assignRole(dto.getUserId(), dto.getRoleIds());
return Result.ok();
}权限缓存
Redis 缓存
java
@Service
public class PermissionService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 获取用户权限列表(带缓存)
*/
public Set<String> getUserPermissions(Long userId) {
String cacheKey = "user:permissions:" + userId;
// 从缓存获取
Set<String> permissions = (Set<String>) redisTemplate.opsForValue().get(cacheKey);
if (permissions != null) {
return permissions;
}
// 从数据库查询
permissions = loadUserPermissions(userId);
// 存入缓存
redisTemplate.opsForValue().set(cacheKey, permissions, 1, TimeUnit.HOURS);
return permissions;
}
/**
* 清除权限缓存
*/
public void clearPermissionCache(Long userId) {
redisTemplate.delete("user:permissions:" + userId);
}
}前端权限控制
权限指令实现
typescript
// directives/permission.ts
import { useUserStore } from '@/stores/user'
const permissionDirective = {
mounted(el: HTMLElement, binding: DirectiveBinding) {
const { value, arg, modifiers } = binding
const userStore = useUserStore()
const permissions = userStore.permissions
const requiredPermissions = Array.isArray(value) ? value : [value]
// 检查是否有权限
const hasPermission = requiredPermissions.some(p => permissions.includes(p))
if (!hasPermission) {
if (arg === 'disabled') {
// 禁用模式
el.disabled = true
el.classList.add('is-disabled')
el.addEventListener('click', preventClick, true)
} else if (modifiers.hide) {
// 隐藏模式
el.style.display = 'none'
} else {
// 默认移除元素
el.parentNode?.removeChild(el)
}
}
},
unmounted(el: HTMLElement, binding: DirectiveBinding) {
if (binding.arg === 'disabled') {
el.removeEventListener('click', preventClick, true)
}
}
}
function preventClick(e: Event) {
e.stopPropagation()
e.preventDefault()
}
export default permissionDirective路由守卫
typescript
// router/guard.ts
router.beforeEach(async (to, from, next) => {
const authStore = useAuthStore()
// 1. 检查 token
if (!authStore.token) {
if (to.path === '/login') {
next()
} else {
next('/login')
}
return
}
// 2. 加载用户信息
if (!authStore.userInfo) {
await authStore.getUserInfo()
}
// 3. 加载动态路由
if (!authStore.isRoutesLoaded) {
await authStore.loadRoutes()
}
// 4. 检查菜单权限
if (to.meta.permission && !authStore.hasPermission(to.meta.permission)) {
next('/403')
return
}
next()
})最佳实践
1. 权限粒度控制
- 菜单权限:控制页面访问
- 按钮权限:控制操作按钮
- 接口权限:控制 API 访问
- 数据权限:控制数据范围
2. 权限命名规范
模块:功能:操作
system:user:add # 系统管理-用户-新增
system:user:edit # 系统管理-用户-编辑
system:user:delete # 系统管理-用户-删除
system:user:export # 系统管理-用户-导出3. 权限设计原则
- 最小权限原则:只授予必要的权限
- 权限分离原则:敏感操作需要多个权限
- 权限审计:记录权限变更日志
4. 敏感操作二次确认
vue
<template>
<el-button
v-permission="'system:user:delete'"
type="danger"
@click="handleDelete"
>
删除
</el-button>
</template>
<script setup>
const handleDelete = () => {
ElMessageBox.confirm(
'确定要删除该用户吗?此操作不可恢复!',
'警告',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
).then(() => {
// 执行删除
})
}
</script>