第 56 章:RBAC 权限模型
学习目标
- 学会 RBAC 数据模型
- 掌握权限注解
- 实现按钮级 / 接口级 / 数据级权限
一、RBAC 是啥
RBAC(Role-Based Access Control):基于角色的访问控制。
用户 → 角色 → 权限
├─ 菜单权限
├─ 按钮权限
└─ 数据权限核心表(5 张):
sql
-- 用户
CREATE TABLE user (
id BIGINT PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL
);
-- 角色
CREATE TABLE role (
id BIGINT PRIMARY KEY,
name VARCHAR(50) NOT NULL
);
-- 权限 / 菜单
CREATE TABLE permission (
id BIGINT PRIMARY KEY,
name VARCHAR(50),
type TINYINT, -- 1=菜单,2=按钮,3=接口
code VARCHAR(100) -- 权限标识,如 user:create
);
-- 用户-角色(多对多)
CREATE TABLE user_role (
user_id BIGINT,
role_id BIGINT,
PRIMARY KEY (user_id, role_id)
);
-- 角色-权限(多对多)
CREATE TABLE role_permission (
role_id BIGINT,
permission_id BIGINT,
PRIMARY KEY (role_id, permission_id)
);示例数据(3 用户 + 3 角色 + 6 权限):
sql
-- 用户表 user
1 | admin
2 | 张三
3 | 李四
-- 角色表 role
1 | 系统管理员(ADMIN)
2 | 部门经理(MANAGER)
3 | 普通员工(EMPLOYEE)
-- 权限表 permission
1 | 创建用户 | 2(按钮) | user:create
2 | 删除用户 | 2(按钮) | user:delete
3 | 查看所有订单 | 3(接口) | order:view_all
4 | 查看本部门订单 | 3(接口) | order:view_dept
5 | 用户管理菜单 | 1(菜单) | menu:user
6 | 订单管理菜单 | 1(菜单) | menu:order
-- 用户-角色
user_id | role_id
1 | 1 -- admin → 管理员
2 | 2 -- 张三 → 部门经理
3 | 3 -- 李四 → 普通员工
-- 角色-权限
role_id | permission_id
1 | 1, 2, 3, 5, 6 -- ADMIN → 全部权限
2 | 4, 5, 6 -- MANAGER → 本部门订单 + 用户/订单菜单
3 | 4, 6 -- EMPLOYEE → 本部门订单 + 订单菜单实际能干啥:
| 用户 | 角色 | 能做 |
|---|---|---|
| admin | 系统管理员 | 所有事(创/删用户、查全部订单) |
| 张三 | 部门经理 | 看本部门订单、看用户菜单(但不能创建/删除用户) |
| 李四 | 普通员工 | 看本部门订单、看订单菜单(连用户菜单都没有) |
便利性:新员工入职只要 INSERT user_role(user_id, role_id),自动拥有该角色所有权限;改角色权限 = 改一批人的权限。
二、权限注解
java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequiresPermission {
String value(); // 权限标识
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequiresRole {
String[] value(); // 角色名
}用法:
java
@RequiresPermission("user:create")
@PostMapping("/users")
public Result<Void> create(@RequestBody UserDTO dto) { }
@RequiresRole("ADMIN")
@DeleteMapping("/users/{id}")
public Result<Void> delete(@PathVariable Long id) { }三、权限拦截器
java
@Aspect
@Component
public class PermissionAspect {
@Around("@annotation(requiresPermission)")
public Object check(ProceedingJoinPoint pjp, RequiresPermission requiresPermission) throws Throwable {
String requiredCode = requiresPermission.value();
Set<String> userPerms = SecurityUtils.getCurrentUserPermissions();
if (!userPerms.contains(requiredCode)) {
throw new BusinessException(ErrorCode.FORBIDDEN);
}
return pjp.proceed();
}
}四、按钮级权限(前端)
json
{
"id": 1,
"username": "admin",
"permissions": ["user:create", "user:update", "user:delete"]
}前端 Vue 3 用法:
vue
<template>
<el-button v-if="hasPermission('user:create')" @click="handleCreate">新建</el-button>
</template>
<script setup>
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
const hasPermission = (code) => userStore.permissions.includes(code)
</script>五、数据权限(行级)
场景:销售只能看自己的订单,经理能看部门的。
sql
-- 注解式
WHERE 1=1
[AND user_id = #{currentUserId}] -- 本人
[AND dept_id = #{currentUserDeptId}] -- 部门
[AND dept_id IN #{currentUserSubDeptIds}] -- 下属部门
[AND 1=1] -- 全部java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DataScope {
int scope(); // 1=本人,2=部门,3=下属,4=全部
}AOP 拦截 mapper(给所有标了 @DataScope 的方法,自动拼 WHERE):
java
@Aspect @Component
public class DataScopeAspect {
@Before("@annotation(dataScope)") // ① 拦截所有带 @DataScope 的方法
public void before(DataScope dataScope) { // dataScope = 被拦截的注解对象(小写名字)
int scope = dataScope.scope();
Long userId = SecurityUtils.getCurrentUserId();
Long deptId = SecurityUtils.getCurrentDeptId();
QueryWrapper<?> q = getCurrentQueryWrapper(); // ② 从 ThreadLocal 拿当前 SQL
switch (scope) {
case 1: q.eq("user_id", userId); break; // 本人
case 2: q.eq("dept_id", deptId); break; // 本部门
case 3: q.in("dept_id", getSubDeptIds(deptId)); break; // 下属部门
case 4: /* 全部:不加条件 */ break;
}
}
}关键点:
@DataScope是自定义注解,贴在 mapper 方法上,声明"我需要数据范围过滤"(类似 48 章的@ValidStatus)。@Before("@annotation(dataScope)")= Spring AOP 前置通知:凡是标了@DataScope的方法,执行前自动把注解对象传进来跑这段代码,动态改 WHERE 条件。- 简单场景业务层手动拼更省事:
java
// 不上 AOP,业务层直接:
List<Order> list = orderMapper.selectList(
new LambdaQueryWrapper<Order>()
.eq(Order::getDeptId, SecurityUtils.getCurrentDeptId())
);@DataScope 贴在哪:贴在 mapper 接口的方法上(不是 service 上),AOP 才能拦截到:
java
public interface OrderMapper extends BaseMapper<Order> {
@DataScope(scope = 1) // 1 = 本人:AOP 自动拼 WHERE user_id = 当前用户ID
List<Order> selectList(QueryWrapper q);
@DataScope(scope = 4) // 4 = 全部:不加条件(管理员用)
List<Order> selectAll(QueryWrapper q);
}六、本章小结
| 要点 | 关键 |
|---|---|
| 5 张表 | user / role / permission / user_role / role_permission |
| 权限粒度 | 菜单 / 按钮 / 接口 / 数据 |
| 注解 | @RequiresPermission / @RequiresRole |
| 按钮权限 | 前端 v-if 判断 |
动手练习
- 设计 RBAC 5 张表,加外键和索引
- 实现
@RequiresPermission切面 - 实现"销售只能看自己订单"的数据权限
下一章:第 57 章:OpenAPI 文档 →