Skip to content

权限管理

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>

基于 MIT 许可发布