第十五章. AOP 切面编程实战:用 RBAC 模型优雅实现权限控制
15.1. 本章导读
本章核心目标:通过一个完整的 RBAC 权限系统实战,掌握 Spring AOP 的核心用法,实现 “一个注解搞定权限校验” 的优雅方案。阅读完本章后,你将能够独立设计并实现一套声明式的权限控制系统,让业务代码彻底摆脱权限校验逻辑的侵入。
15.1.1. 学习路径与认知里程碑
本章内容较为系统,我们将按照以下路径逐步推进:
阶段一:理论奠基(15.2)
- 学习目标:理解 RBAC 模型的设计思想
- 认知里程碑:能够独立设计用户-角色-权限的数据库表结构
阶段二:基础组件搭建(15.3 - 15.4)
- 学习目标:掌握自定义注解的创建方法,完成数据访问层开发
- 认知里程碑:能够创建带属性的自定义注解,能够编写多表关联查询
阶段三:AOP 核心实战(15.5)
- 学习目标:掌握 Spring AOP 切面的完整编写流程
- 认知里程碑:能够独立实现基于注解的方法拦截与权限校验
阶段四:集成验证(15.6)
- 学习目标:将所有组件整合并进行全面测试
- 认知里程碑:能够独立排查权限校验失败的问题
阶段五:生产级优化(15.7 - 15.8)
- 学习目标:掌握全局异常处理和多权限校验的扩展方案
- 认知里程碑:能够将权限系统应用于真实生产环境
15.1.2. 环境版本清单
在开始编码之前,请确保你的开发环境与以下版本保持一致,避免因版本差异导致的兼容性问题:
| 组件 | 版本 | 说明 |
|---|
| JDK | 17+ | 使用 LTS 长期支持版本 |
| Spring Boot | 3.2.x | 当前主流稳定版本 |
| MyBatis-Plus | 3.5.x | ORM 增强框架 |
| H2 Database | 2.2.x | 内嵌数据库,便于演示 |
| Lombok | 1.18.x | 简化样板代码 |
15.2. RBAC 权限模型详解
在动手编码之前,我们必须先理解企业软件权限管理中最主流的模型——RBAC。不理解其设计思想,写出的代码也只是空中楼阁。
15.2.1. 从 “用户-权限” 到 “用户-角色-权限” 的演进
RBAC,即 基于角色的访问控制(Role-Based Access Control)。它的核心思想非常简单,但在用户和权限之间增加了一个至关重要的中间层:“角色”。
传统的权限管理是 “用户-权限” 直接关联。

这种模式在用户和权限数量少的时候尚可应付。但想象一个拥有 1000 名员工和 200 个操作权限(如 “查看报表”、“编辑商品”、“审核订单” 等)的公司。如果新入职一位销售人员,管理员需要手动为他勾选上他应该拥有的几十个权限。如果一位销售经理离职,管理员又需要将他的权限一一移除。这无疑是低效且易错的。
RBAC 的引入,将授权逻辑优雅地分解为两步:
为角色分配权限:定义好 “销售”、“经理”、“财务”、“技术支持” 等角色,并为每个角色配置好它们应该拥有的权限集合。例如,“销售” 角色拥有 “查看商品”、“创建订单” 权限;“经理” 角色则额外拥有 “审核订单”、“查看销售报表” 的权限。这一步通常由系统管理员完成,且不经常变动。
为用户分配角色:当新员工入职时,不再需要关心具体的权限点,只需给他分配一个或多个预设好的角色。例如,给新来的销售分配一个 “销售” 角色,他就自动继承了该角色所关联的所有权限。

通过引入 “角色” 这个中间层,RBAC 实现了用户与权限的解耦。人事变动时,我们只需要调整用户与角色的关系,而无需触碰背后复杂的权限配置,极大地简化了权限管理和维护的复杂度。
15.2.2. 五张核心表的设计思路与 ER 图
一个标准的 RBAC 系统包含五张核心表,它们共同构成了一个灵活且强大的权限体系:

表结构说明:
- sys_user(用户表):存储系统用户的基本信息。
- sys_role(角色表):定义系统中的所有角色,如 “管理员”、“普通用户”、“访客” 等。
- sys_permission(权限表):定义系统中的所有操作权限,如 “user: delete”、“order: create” 等。
- sys_user_role(用户-角色关联表):一个用户可以拥有多个角色,一个角色可以分配给多个用户,这是典型的多对多关系。
- sys_role_permission(角色-权限关联表):一个角色可以拥有多个权限,一个权限可以分配给多个角色,同样是多对多关系。
15.2.3. H2 数据库建表脚本
以下是完整的建表脚本,我们使用 H2 内嵌数据库进行演示:
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 59 60 61 62 63 64 65 66 67 68 69 70 71
| DROP TABLE IF EXISTS sys_user; CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); COMMENT ON TABLE sys_user IS '用户表'; COMMENT ON COLUMN sys_user.id IS '用户ID'; COMMENT ON COLUMN sys_user.username IS '用户名'; COMMENT ON COLUMN sys_user.password IS '密码(实际项目中需加密)'; COMMENT ON COLUMN sys_user.create_time IS '创建时间';
DROP TABLE IF EXISTS sys_role; CREATE TABLE sys_role ( id BIGINT PRIMARY KEY AUTO_INCREMENT, role_code VARCHAR(50) NOT NULL UNIQUE, role_name VARCHAR(100) NOT NULL, description VARCHAR(255) ); COMMENT ON TABLE sys_role IS '角色表'; COMMENT ON COLUMN sys_role.id IS '角色ID'; COMMENT ON COLUMN sys_role.role_code IS '角色编码(如 ADMIN, USER)'; COMMENT ON COLUMN sys_role.role_name IS '角色名称(如 系统管理员)'; COMMENT ON COLUMN sys_role.description IS '角色描述';
DROP TABLE IF EXISTS sys_permission; CREATE TABLE sys_permission ( id BIGINT PRIMARY KEY AUTO_INCREMENT, permission_code VARCHAR(100) NOT NULL UNIQUE, permission_name VARCHAR(100) NOT NULL, resource_type VARCHAR(20) ); COMMENT ON TABLE sys_permission IS '权限表'; COMMENT ON COLUMN sys_permission.id IS '权限ID'; COMMENT ON COLUMN sys_permission.permission_code IS '权限编码(如 user:delete)'; COMMENT ON COLUMN sys_permission.permission_name IS '权限名称(如 删除用户)'; COMMENT ON COLUMN sys_permission.resource_type IS '资源类型(如 MENU, BUTTON, API)';
DROP TABLE IF EXISTS sys_user_role; CREATE TABLE sys_user_role ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, role_id BIGINT NOT NULL, CONSTRAINT uk_user_role UNIQUE (user_id, role_id), FOREIGN KEY (user_id) REFERENCES sys_user(id) ON DELETE CASCADE, FOREIGN KEY (role_id) REFERENCES sys_role(id) ON DELETE CASCADE ); COMMENT ON TABLE sys_user_role IS '用户-角色关联表'; COMMENT ON COLUMN sys_user_role.id IS '主键ID'; COMMENT ON COLUMN sys_user_role.user_id IS '用户ID'; COMMENT ON COLUMN sys_user_role.role_id IS '角色ID';
DROP TABLE IF EXISTS sys_role_permission; CREATE TABLE sys_role_permission ( id BIGINT PRIMARY KEY AUTO_INCREMENT, role_id BIGINT NOT NULL, permission_id BIGINT NOT NULL, CONSTRAINT uk_role_permission UNIQUE (role_id, permission_id), FOREIGN KEY (role_id) REFERENCES sys_role(id) ON DELETE CASCADE, FOREIGN KEY (permission_id) REFERENCES sys_permission(id) ON DELETE CASCADE ); COMMENT ON TABLE sys_role_permission IS '角色-权限关联表'; COMMENT ON COLUMN sys_role_permission.id IS '主键ID'; COMMENT ON COLUMN sys_role_permission.role_id IS '角色ID'; COMMENT ON COLUMN sys_role_permission.permission_id IS '权限ID';
|
15.2.4. 本节小结
本节我们深入理解了 RBAC 权限模型的设计思想,核心要点如下:
| 概念 | 说明 | 数据库表 |
|---|
| 用户 | 系统的使用者 | sys_user |
| 角色 | 权限的集合,是用户与权限之间的桥梁 | sys_role |
| 权限 | 对资源的操作许可 | sys_permission |
| 用户-角色关联 | 多对多关系 | sys_user_role |
| 角色-权限关联 | 多对多关系 | sys_role_permission |
15.3. 自定义注解 @RequiresPermission
理解了 RBAC 模型后,我们面临一个问题:如何在代码中优雅地声明 “访问这个接口需要什么权限”?答案是使用自定义注解。通过在方法上添加一个简单的标记,我们就能声明该方法的权限要求,而具体的校验逻辑则交给 AOP 切面处理。
15.3.1. 元注解 @Target 与 @Retention 的作用
在创建自定义注解之前,我们需要了解两个关键的元注解(用于修饰注解的注解):
@Target:指定注解可以标注在哪些程序元素上。常用的取值包括:
ElementType.TYPE:类、接口、枚举ElementType.METHOD:方法ElementType.FIELD:字段ElementType.PARAMETER:方法参数
@Retention:指定注解的保留策略,即注解在什么阶段仍然有效。取值包括:
RetentionPolicy.SOURCE:仅在源码中保留,编译时丢弃RetentionPolicy.CLASS:保留到字节码文件,但运行时不可见RetentionPolicy.RUNTIME:运行时仍然可见,可通过反射读取
对于 AOP 权限校验场景,我们需要在运行时读取注解信息,因此必须使用 RetentionPolicy.RUNTIME。
15.3.2. 注解属性设计:value() 的命名约定
我们的注解需要承载一个信息:“访问此方法需要什么权限?”。因此,它需要一个属性来接收这个权限字符串。
📄 文件:src/main/java/com/example/demo/annotation/RequiresPermission.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| package com.example.demo.annotation;
import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target;
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface RequiresPermission {
String value(); }
|
这个注解本身没有任何逻辑,它就像一个 “便利贴”,安静地贴在需要保护的方法上,等待着 AOP 这个 “扫描器” 来读取并执行相应的逻辑。
15.3.3. 本节小结
| 要点 | 说明 |
|---|
| @Target(ElementType.METHOD) | 限制注解只能用于方法 |
| @Retention(RetentionPolicy.RUNTIME) | 确保运行时可通过反射读取 |
| value() 属性 | 使用默认属性名,简化注解使用语法 |
15.4. 数据访问层:Entity 与 Mapper
在编写核心切面之前,我们需要先为五张数据库表创建对应的实体类和 Mapper 接口,这样才能在 AOP 中查询用户的权限信息。
15.4.1. 五张表对应的实体类
📄 文件:src/main/java/com/example/demo/entity/User.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| package com.example.demo.entity;
import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import java.time.LocalDateTime;
@Data @TableName("sys_user") public class User { @TableId(type = IdType.AUTO) private Long id; private String username; private String password; private LocalDateTime createTime; }
|
📄 文件:src/main/java/com/example/demo/entity/Role.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| package com.example.demo.entity;
import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data;
@Data @TableName("sys_role") public class Role { @TableId(type = IdType.AUTO) private Long id; private String roleCode; private String roleName; private String description; }
|
📄 文件:src/main/java/com/example/demo/entity/Permission.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| package com.example.demo.entity;
import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data;
@Data @TableName("sys_permission") public class Permission { @TableId(type = IdType.AUTO) private Long id; private String permissionCode; private String permissionName; private String resourceType; }
|
📄 文件:src/main/java/com/example/demo/entity/UserRole.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| package com.example.demo.entity;
import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data;
@Data @TableName("sys_user_role") public class UserRole { @TableId(type = IdType.AUTO) private Long id; private Long userId; private Long roleId; }
|
📄 文件:src/main/java/com/example/demo/entity/RolePermission.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| package com.example.demo.entity;
import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data;
@Data @TableName("sys_role_permission") public class RolePermission { @TableId(type = IdType.AUTO) private Long id; private Long roleId; private Long permissionId; }
|
15.4.2. 权限查询的核心 SQL:三表连接
📄 文件:src/main/java/com/example/demo/mapper/UserMapper.java
1 2 3 4 5 6 7 8 9
| package com.example.demo.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.example.demo.entity.User; import org.apache.ibatis.annotations.Mapper;
@Mapper public interface UserMapper extends BaseMapper<User> { }
|
📄 文件:src/main/java/com/example/demo/mapper/RoleMapper.java
1 2 3 4 5 6 7 8 9
| package com.example.demo.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.example.demo.entity.Role; import org.apache.ibatis.annotations.Mapper;
@Mapper public interface RoleMapper extends BaseMapper<Role> { }
|
📄 文件:src/main/java/com/example/demo/mapper/PermissionMapper.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| package com.example.demo.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.example.demo.entity.Permission; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; import java.util.List;
@Mapper public interface PermissionMapper extends BaseMapper<Permission> {
@Select("SELECT DISTINCT p.permission_code " + "FROM sys_permission p " + "INNER JOIN sys_role_permission rp ON p.id = rp.permission_id " + "INNER JOIN sys_user_role ur ON rp.role_id = ur.role_id " + "WHERE ur.user_id = #{userId}") List<String> selectPermissionsByUserId(@Param("userId") Long userId); }
|
📄 文件:src/main/java/com/example/demo/mapper/UserRoleMapper.java
1 2 3 4 5 6 7 8 9
| package com.example.demo.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.example.demo.entity.UserRole; import org.apache.ibatis.annotations.Mapper;
@Mapper public interface UserRoleMapper extends BaseMapper<UserRole> { }
|
📄 文件:src/main/java/com/example/demo/mapper/RolePermissionMapper.java
1 2 3 4 5 6 7 8 9
| package com.example.demo.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.example.demo.entity.RolePermission; import org.apache.ibatis.annotations.Mapper;
@Mapper public interface RolePermissionMapper extends BaseMapper<RolePermission> { }
|
最关键的是 PermissionMapper 中的 selectPermissionsByUserId 方法。这个方法通过一个三表连接查询,实现了从用户 ID 到权限编码的完整路径:

SQL 的执行逻辑:
- 从
sys_user_role 表中找到该用户拥有的所有角色 ID - 从
sys_role_permission 表中找到这些角色拥有的所有权限 ID - 从
sys_permission 表中获取这些权限 ID 对应的权限编码 - 使用
DISTINCT 去重,因为一个用户可能通过不同角色获得相同的权限
15.4.3. 本节小结
| 组件 | 作用 | 关键点 |
|---|
| 五个 Entity | 映射数据库表 | 使用 @TableName 指定表名 |
| UserMapper | 查询用户信息 | 继承 BaseMapper 获得 CRUD |
| PermissionMapper | 查询用户权限 | 三表连接 + DISTINCT 去重 |
15.5. 权限校验切面 PermissionCheckAspect
注解定义好了,现在我们需要创建真正的 “卫兵”——切面类。这个切面将专门负责拦截所有被 @RequiresPermission 注解标记的方法,并在方法执行前进行权限校验。这是本章最核心的代码,我们将分步构建它。
15.5.1. 切面骨架:@Aspect 与 @Before
首先,我们创建 PermissionCheckAspect 类,并为其添加必要的 Spring AOP 注解。
📄 文件:src/main/java/com/example/demo/aspect/PermissionCheckAspect.java
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
| package com.example.demo.aspect;
import com.example.demo.annotation.RequiresPermission; import com.example.demo.mapper.PermissionMapper; import com.example.demo.mapper.UserMapper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.springframework.stereotype.Component;
@Aspect @Component @Slf4j @RequiredArgsConstructor public class PermissionCheckAspect {
private final UserMapper userMapper;
private final PermissionMapper permissionMapper;
@Before("@annotation(requiresPermission)") public void checkPermission(JoinPoint joinPoint, RequiresPermission requiresPermission) { log.info("====== 进入 AOP 权限校验 ======");
} }
|
15.5.2. 切点表达式:@annotation 的绑定技巧
在上面的代码中,@Before("@annotation(requiresPermission)") 这行代码蕴含着一个非常实用的技巧:注解绑定。
常规写法是 @Before("@annotation(com.example.demo.annotation.RequiresPermission)"),这种写法只能匹配被该注解标记的方法,但无法在通知方法中直接获取注解实例。
而我们使用的写法 @Before("@annotation(requiresPermission)") 中,requiresPermission 是通知方法的参数名。Spring AOP 会自动将匹配到的注解实例注入到这个参数中,让我们可以直接调用 requiresPermission.value() 获取注解的属性值。
15.5.3. 获取 HTTP 上下文:RequestContextHolder
权限校验的第一步,是明确 “当前是谁在访问”。在 Web 应用中,用户信息通常包含在 HTTP 请求中。我们将模拟一个常见的场景:从请求头中获取用户 ID。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
|
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); if (attributes == null) { log.warn("非 HTTP 请求上下文,跳过权限校验"); return; } HttpServletRequest request = attributes.getRequest();
String userIdStr = request.getHeader("X-User-Id"); if (userIdStr == null || userIdStr.isEmpty()) { log.error("权限校验失败:请求头缺少 X-User-Id"); throw new RuntimeException("401 Unauthorized: 请求头缺少 X-User-Id,请先登录"); }
Long userId = Long.valueOf(userIdStr); log.info("当前请求用户 ID: {}", userId);
|
RequestContextHolder 是 Spring 提供的一个工具类,它可以帮助我们在应用的任何地方获取到与当前线程绑定的 HttpServletRequest 对象。这是在非 Controller 层获取请求信息的标准做法。
request.getHeader("X-User-Id") 表示我们约定,前端在调用需要权限的接口时,必须在 HTTP 请求头(Header)中携带一个名为 X-User-Id 的字段,其值为当前登录用户的 ID。这是一种无状态的、对 RESTful API 友好的身份传递方式。
15.5.4. 权限比对与异常中断
获取到用户 ID 后,我们需要验证用户是否存在,查询该用户拥有的所有权限,然后与目标方法要求的权限进行比对。
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
|
User user = userMapper.selectById(userId); if (user == null) { log.error("权限校验失败:用户不存在,ID: {}", userId); throw new RuntimeException("404 Not Found: 用户不存在,ID: " + userId); }
log.info("查询到用户:{}", user.getUsername());
List<String> userPermissions = permissionMapper.selectPermissionsByUserId(userId);
log.info("用户 {} 拥有的权限列表: {}", user.getUsername(), userPermissions);
String requiredPermission = requiresPermission.value();
log.info("目标接口需要的权限: {}", requiredPermission);
if (userPermissions == null || !userPermissions.contains(requiredPermission)) { log.error("权限校验失败:用户 {} 缺少权限 {}", user.getUsername(), requiredPermission); throw new RuntimeException("403 Forbidden: 权限不足,需要权限: " + requiredPermission); }
log.info("====== 权限校验通过!用户 {} 拥有权限 {} ======", user.getUsername(), requiredPermission);
|
这段代码中有几个关键点需要理解:
userMapper.selectById(userId) 调用 MyBatis-Plus 提供的 selectById 方法,根据用户 ID 从数据库中高效地查询出 User 对象。
permissionMapper.selectPermissionsByUserId(userId) 是关键的一步。通过我们之前定义的三表连接查询,一次性获取该用户通过其所有角色继承而来的全部权限编码列表。例如,如果用户同时拥有 ADMIN 和 USER 两个角色,这个列表会包含这两个角色的所有权限的并集。
requiresPermission.value() 还记得我们在注解中定义的 String value() 吗?在这里,我们通过 requiresPermission 这个由 AOP 注入的注解实例,直接调用 value() 方法,就轻松获取到了标注在具体方法上的权限编码字符串,例如 “user: delete”。
userPermissions.contains(requiredPermission) 是最核心的权限判断逻辑。我们检查用户的权限列表中是否包含目标权限。这里使用的是精确匹配,实际项目中可能还需要支持通配符匹配(如 user:* 表示所有 user 相关权限)。
throw new RuntimeException(...) 是 AOP 中断方法执行的关键。一旦在前置通知中抛出任何异常,AOP 将会立即停止执行流程,目标业务方法(如 deleteData)将完全没有机会被调用。这就像卫兵发现入侵者后直接拉响警报并关闭大门,确保了核心区域的安全。
15.5.5. 完整代码整合
现在让我们将所有代码片段整合在一起,形成完整的 PermissionCheckAspect 类:
📄 文件:src/main/java/com/example/demo/aspect/PermissionCheckAspect.java
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 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88
| package com.example.demo.aspect;
import com.example.demo.annotation.RequiresPermission; import com.example.demo.entity.User; import com.example.demo.mapper.PermissionMapper; import com.example.demo.mapper.UserMapper; import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes;
import java.util.List;
@Aspect @Component @Slf4j @RequiredArgsConstructor public class PermissionCheckAspect {
private final UserMapper userMapper;
private final PermissionMapper permissionMapper;
@Before("@annotation(requiresPermission)") public void checkPermission(JoinPoint joinPoint, RequiresPermission requiresPermission) { log.info("====== 进入 AOP 权限校验 ======");
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); if (attributes == null) { log.warn("非 HTTP 请求上下文,跳过权限校验"); return; } HttpServletRequest request = attributes.getRequest(); String userIdStr = request.getHeader("X-User-Id"); if (userIdStr == null || userIdStr.isEmpty()) { log.error("权限校验失败:请求头缺少 X-User-Id"); throw new RuntimeException("401 Unauthorized: 请求头缺少 X-User-Id,请先登录"); } Long userId = Long.valueOf(userIdStr); log.info("当前请求用户 ID: {}", userId);
User user = userMapper.selectById(userId); if (user == null) { log.error("权限校验失败:用户不存在,ID: {}", userId); throw new RuntimeException("404 Not Found: 用户不存在,ID: " + userId); } log.info("查询到用户:{}", user.getUsername()); List<String> userPermissions = permissionMapper.selectPermissionsByUserId(userId); log.info("用户 {} 拥有的权限列表: {}", user.getUsername(), userPermissions);
String requiredPermission = requiresPermission.value();
log.info("目标接口需要的权限: {}", requiredPermission);
if (userPermissions == null || !userPermissions.contains(requiredPermission)) { log.error("权限校验失败:用户 {} 缺少权限 {}", user.getUsername(), requiredPermission); throw new RuntimeException("403 Forbidden: 权限不足,需要权限: " + requiredPermission); } log.info("====== 权限校验通过!用户 {} 拥有权限 {} ======", user.getUsername(), requiredPermission);
} }
|
至此,我们最核心的 PermissionCheckAspect 切面已经编写完毕。它像一个忠诚的、自动化的卫兵,守护着所有被 @RequiresPermission 标记的 “城门”。
15.5.6. 本节小结
| 组件 | 作用 | 关键代码 |
|---|
| @Aspect | 声明切面类 | 类级别注解 |
| @Before | 前置通知,方法执行前触发 | @Before("@annotation(requiresPermission)") |
| RequestContextHolder | 获取 HTTP 请求上下文 | RequestContextHolder.getRequestAttributes() |
| 注解绑定 | 获取注解实例及属性 | 参数名与切点表达式对应 |
| 异常中断 | 阻止目标方法执行 | throw new RuntimeException(...) |
15.6. Controller 层应用与测试验证
万事俱备,只欠东风。现在我们来创建一个 AdminController,并在其方法上使用我们亲手打造的 @RequiresPermission 注解,来验证我们的 AOP 卫兵是否能够正常工作。
15.6.1. 测试接口设计:五种权限场景
📄 文件:src/main/java/com/example/demo/controller/AdminController.java
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
| package com.example.demo.controller;
import com.example.demo.annotation.RequiresPermission; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController;
@RestController @RequestMapping("/admin") public class AdminController {
@GetMapping("/delete-user") @RequiresPermission("user:delete") public String deleteUser() { return "【管理员专属】用户删除成功!"; }
@GetMapping("/delete-data") @RequiresPermission("data:delete") public String deleteData() { return "【管理员专属】敏感数据删除成功!"; }
@GetMapping("/view-user") @RequiresPermission("user:view") public String viewUser() { return "【普通用户可访问】用户信息查看成功。"; }
@GetMapping("/view-data") @RequiresPermission("data:view") public String viewData() { return "【普通用户可访问】数据查看成功。"; }
@GetMapping("/public-info") public String getPublicInfo() { return "这是一个公开接口,无需特定权限。"; } }
|
我们创建了 5 个接口,分别对应不同的权限需求:
deleteUser:需要 user:delete 权限(只有 ADMIN 角色拥有)deleteData:需要 data:delete 权限(只有 ADMIN 角色拥有)viewUser:需要 user:view 权限(ADMIN 和 USER 角色都拥有)viewData:需要 data:view 权限(所有角色都拥有)getPublicInfo:无需任何权限
这样的设计让我们可以测试不同角色访问不同接口时的权限控制效果。
15.6.2. 六个测试用例与预期结果
现在,启动 Spring Boot 应用。我们的数据库中已经通过初始化脚本准备好了测试数据:
- 用户 1 (admin):拥有 ADMIN 角色,具备所有权限
- 用户 2 (testuser):拥有 USER 角色,只有
user:view 和 data:view 权限 - 用户 3 (guest):拥有 GUEST 角色,只有
data:view 权限
我们将使用 curl 命令行工具来模拟不同用户的请求。
场景一:管理员访问删除接口(预期:成功)
1
| curl -H "X-User-Id: 1" http://localhost:8080/admin/delete-user
|
预期控制台日志:
1 2 3 4 5 6
| INFO --- [nio-8080-exec-1] c.e.d.aspect.PermissionCheckAspect : ====== 进入 AOP 权限校验 ====== INFO --- [nio-8080-exec-1] c.e.d.aspect.PermissionCheckAspect : 当前请求用户 ID: 1 INFO --- [nio-8080-exec-1] c.e.d.aspect.PermissionCheckAspect : 查询到用户:admin INFO --- [nio-8080-exec-1] c.e.d.aspect.PermissionCheckAspect : 用户 admin 拥有的权限列表: [user:view, user:create, user:update, user:delete, data:view, data:delete] INFO --- [nio-8080-exec-1] c.e.d.aspect.PermissionCheckAspect : 目标接口需要的权限: user:delete INFO --- [nio-8080-exec-1] c.e.d.aspect.PermissionCheckAspect : ====== 权限校验通过!用户 admin 拥有权限 user:delete ======
|
预期 HTTP 响应:
✅ 测试通过:管理员拥有所有权限,可以成功访问。
场景二:普通用户访问删除接口(预期:失败)
1
| curl -H "X-User-Id: 2" http://localhost:8080/admin/delete-user
|
预期控制台日志:
1 2 3 4 5 6 7 8 9 10
| INFO --- [nio-8080-exec-2] c.e.d.aspect.PermissionCheckAspect : ====== 进入 AOP 权限校验 ====== INFO --- [nio-8080-exec-2] c.e.d.aspect.PermissionCheckAspect : 当前请求用户 ID: 2 INFO --- [nio-8080-exec-2] c.e.d.aspect.PermissionCheckAspect : 查询到用户:testuser INFO --- [nio-8080-exec-2] c.e.d.aspect.PermissionCheckAspect : 用户 testuser 拥有的权限列表: [user:view, data:view] INFO --- [nio-8080-exec-2] c.e.d.aspect.PermissionCheckAspect : 目标接口需要的权限: user:delete ERROR --- [nio-8080-exec-2] c.e.d.aspect.PermissionCheckAspect : 权限校验失败:用户 testuser 缺少权限 user:delete ERROR --- [nio-8080-exec-2] o.a.c.c.C.[.[.[/].[dispatcherServlet]: Servlet.service() for servlet [dispatcherServlet] threw exception java.lang.RuntimeException: 403 Forbidden: 权限不足,需要权限: user:delete at com.example.demo.aspect.PermissionCheckAspect.checkPermission(PermissionCheckAspect.java:69) ...
|
预期 HTTP 响应:
1 2 3
| HTTP/1.1 500 Internal Server Error
java.lang.RuntimeException: 403 Forbidden: 权限不足,需要权限: user:delete
|
✅ 测试通过:普通用户没有 user:delete 权限,被成功拦截。目标方法 deleteUser() 完全没有被执行!
场景三:普通用户访问查看接口(预期:成功)
1
| curl -H "X-User-Id: 2" http://localhost:8080/admin/view-user
|
预期控制台日志:
1 2 3 4 5 6
| INFO --- [nio-8080-exec-3] c.e.d.aspect.PermissionCheckAspect : ====== 进入 AOP 权限校验 ====== INFO --- [nio-8080-exec-3] c.e.d.aspect.PermissionCheckAspect : 当前请求用户 ID: 2 INFO --- [nio-8080-exec-3] c.e.d.aspect.PermissionCheckAspect : 查询到用户:testuser INFO --- [nio-8080-exec-3] c.e.d.aspect.PermissionCheckAspect : 用户 testuser 拥有的权限列表: [user:view, data:view] INFO --- [nio-8080-exec-3] c.e.d.aspect.PermissionCheckAspect : 目标接口需要的权限: user:view INFO --- [nio-8080-exec-3] c.e.d.aspect.PermissionCheckAspect : ====== 权限校验通过!用户 testuser 拥有权限 user:view ======
|
预期 HTTP 响应:
✅ 测试通过:普通用户拥有 user:view 权限,可以成功访问。
场景四:访客访问数据查看接口(预期:成功)
1
| curl -H "X-User-Id: 3" http://localhost:8080/admin/view-data
|
预期控制台日志:
1 2 3 4 5 6
| INFO --- [nio-8080-exec-4] c.e.d.aspect.PermissionCheckAspect : ====== 进入 AOP 权限校验 ====== INFO --- [nio-8080-exec-4] c.e.d.aspect.PermissionCheckAspect : 当前请求用户 ID: 3 INFO --- [nio-8080-exec-4] c.e.d.aspect.PermissionCheckAspect : 查询到用户:guest INFO --- [nio-8080-exec-4] c.e.d.aspect.PermissionCheckAspect : 用户 guest 拥有的权限列表: [data:view] INFO --- [nio-8080-exec-4] c.e.d.aspect.PermissionCheckAspect : 目标接口需要的权限: data:view INFO --- [nio-8080-exec-4] c.e.d.aspect.PermissionCheckAspect : ====== 权限校验通过!用户 guest 拥有权限 data:view ======
|
预期 HTTP 响应:
✅ 测试通过:访客拥有 data:view 权限,可以成功访问。
场景五:访客访问删除接口(预期:失败)
1
| curl -H "X-User-Id: 3" http://localhost:8080/admin/delete-data
|
预期控制台日志:
1 2 3 4 5 6 7 8 9
| INFO --- [nio-8080-exec-5] c.e.d.aspect.PermissionCheckAspect : ====== 进入 AOP 权限校验 ====== INFO --- [nio-8080-exec-5] c.e.d.aspect.PermissionCheckAspect : 当前请求用户 ID: 3 INFO --- [nio-8080-exec-5] c.e.d.aspect.PermissionCheckAspect : 查询到用户:guest INFO --- [nio-8080-exec-5] c.e.d.aspect.PermissionCheckAspect : 用户 guest 拥有的权限列表: [data:view] INFO --- [nio-8080-exec-5] c.e.d.aspect.PermissionCheckAspect : 目标接口需要的权限: data:delete ERROR --- [nio-8080-exec-5] c.e.d.aspect.PermissionCheckAspect : 权限校验失败:用户 guest 缺少权限 data:delete ERROR --- [nio-8080-exec-5] o.a.c.c.C.[.[.[/].[dispatcherServlet]: Servlet.service() threw exception java.lang.RuntimeException: 403 Forbidden: 权限不足,需要权限: data:delete ...
|
预期 HTTP 响应:
1 2 3
| HTTP/1.1 500 Internal Server Error
java.lang.RuntimeException: 403 Forbidden: 权限不足,需要权限: data:delete
|
✅ 测试通过:访客只有只读权限,无法执行删除操作,被成功拦截。
场景六:访问公开接口(预期:成功)
1
| curl -H "X-User-Id: 3" http://localhost:8080/admin/public-info
|
预期控制台日志:
1
| (没有 AOP 相关日志,因为该方法没有被 @RequiresPermission 注解标记)
|
预期 HTTP 响应:
✅ 测试通过:没有注解的方法不会被 AOP 拦截,任何人都可以访问。
通过以上六个场景的测试,我们验证了:

实验全部成功!我们的 AOP 权限卫兵完美地履行了职责,将不合法的访问请求拦截在了业务逻辑的大门之外。我们真正实现了权限逻辑与业务逻辑的彻底解耦。
15.6.3. 本节小结
| 测试场景 | 用户 | 目标权限 | 用户权限 | 预期结果 |
|---|
| 场景一 | admin | user: delete | 全部权限 | ✅ 成功 |
| 场景二 | testuser | user: delete | user: view, data: view | ❌ 拒绝 |
| 场景三 | testuser | user: view | user: view, data: view | ✅ 成功 |
| 场景四 | guest | data: view | data: view | ✅ 成功 |
| 场景五 | guest | data: delete | data: view | ❌ 拒绝 |
| 场景六 | guest | 无注解 | - | ✅ 成功 |
15.7. 全局异常处理:统一错误响应
你可能注意到,当权限校验失败时,返回的是一个 500 错误和完整的堆栈信息。这在生产环境中是不合适的。我们应该返回更友好、更安全的错误响应。
15.7.1. 为什么 500 错误不能直接暴露?
在前面的测试中,当权限校验失败时,服务器返回了 HTTP 500 状态码和完整的 Java 堆栈信息。这存在两个严重问题:
安全风险:堆栈信息暴露了服务器内部的类名、方法名、行号等敏感信息,攻击者可以利用这些信息寻找系统漏洞。
语义错误:HTTP 500 表示 “服务器内部错误”,但权限不足应该返回 403(Forbidden),用户未登录应该返回 401(Unauthorized)。错误的状态码会误导前端的错误处理逻辑。
15.7.2. @RestControllerAdvice 与状态码映射
📄 文件:src/main/java/com/example/demo/exception/GlobalExceptionHandler.java
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
| package com.example.demo.exception;
import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.HashMap; import java.util.Map;
@RestControllerAdvice @Slf4j public class GlobalExceptionHandler {
@ExceptionHandler(RuntimeException.class) public ResponseEntity<Map<String, Object>> handleRuntimeException(RuntimeException e) { log.error("捕获到运行时异常: {}", e.getMessage());
String message = e.getMessage(); HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR;
if (message != null) { if (message.startsWith("401")) { status = HttpStatus.UNAUTHORIZED; } else if (message.startsWith("403")) { status = HttpStatus.FORBIDDEN; } else if (message.startsWith("404")) { status = HttpStatus.NOT_FOUND; } }
Map<String, Object> errorResponse = new HashMap<>(); errorResponse.put("success", false); errorResponse.put("code", status.value()); errorResponse.put("message", message); errorResponse.put("timestamp", System.currentTimeMillis());
return new ResponseEntity<>(errorResponse, status); } }
|
这段代码中有几个关键点:
@RestControllerAdvice 是 Spring 提供的全局异常处理注解,标注在类上表示这个类会处理所有 Controller 抛出的异常。
@ExceptionHandler(RuntimeException.class) 指定这个方法专门处理 RuntimeException 类型的异常。
智能状态码识别:我们通过检查异常消息的前缀(“401”、“403”、“404”)来决定返回什么 HTTP 状态码,这样前端就能根据标准的 HTTP 状态码做出正确的响应(如跳转到登录页、显示权限不足提示等)。
统一响应格式:返回一个包含 success、code、message、timestamp 的 JSON 对象,这是 RESTful API 的最佳实践。
现在再次测试场景二(普通用户访问删除接口),响应将变得更加友好:
1
| curl -i -H "X-User-Id: 2" http://localhost:8080/admin/delete-user
|
优化后的响应:
1 2 3 4 5 6 7 8 9
| HTTP/1.1 403 Forbidden Content-Type: application/json
{ "success": false, "code": 403, "message": "403 Forbidden: 权限不足,需要权限: user:delete", "timestamp": 1702732800000 }
|
这样的响应既专业又安全,不会泄露服务器内部的堆栈信息。
15.7.3. 本节小结
| 要点 | 说明 |
|---|
| @RestControllerAdvice | 全局异常处理器,捕获所有 Controller 异常 |
| @ExceptionHandler | 指定处理的异常类型 |
| 状态码映射 | 401 未认证、403 权限不足、404 资源不存在 |
| 统一响应格式 | success、code、message、timestamp |
15.8. 多权限校验:AND 与 OR 逻辑扩展
在实际业务中,一个接口可能需要多个权限才能访问(如同时需要 “查看” 和 “编辑” 权限),或者满足其中一个权限即可访问。我们可以对注解进行扩展来支持这些场景。
15.8.1. 注解改造:支持 String [] 与 LogicalOperator
📄 文件:src/main/java/com/example/demo/annotation/RequiresPermission.java(修改版)
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
| package com.example.demo.annotation;
import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target;
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface RequiresPermission {
String[] value();
LogicalOperator logical() default LogicalOperator.AND;
enum LogicalOperator { AND, OR } }
|
这次改造的关键变化:
value() 的返回类型从 String 改为 String[],支持传入多个权限- 新增
logical() 属性,用于指定多个权限之间的逻辑关系 - 定义了
LogicalOperator 枚举,包含 AND 和 OR 两种逻辑
15.8.2. 切面改造:containsAll 与 anyMatch
对应地,我们需要修改切面中的权限校验逻辑:
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
|
String[] requiredPermissions = requiresPermission.value(); RequiresPermission.LogicalOperator logical = requiresPermission.logical();
log.info("目标接口需要的权限: {}, 逻辑关系: {}", String.join(", ", requiredPermissions), logical);
boolean hasPermission = false;
if (logical == RequiresPermission.LogicalOperator.AND) { hasPermission = userPermissions != null && userPermissions.containsAll(java.util.Arrays.asList(requiredPermissions)); } else { if (userPermissions != null) { for (String required : requiredPermissions) { if (userPermissions.contains(required)) { hasPermission = true; break; } } } }
if (!hasPermission) { log.error("权限校验失败:用户 {} 缺少权限 {}", user.getUsername(), String.join(" " + logical + " ", requiredPermissions)); throw new RuntimeException("403 Forbidden: 权限不足,需要权限: " + String.join(" " + logical + " ", requiredPermissions)); }
log.info("====== 权限校验通过!======");
|
这段代码的核心逻辑:
AND 逻辑:使用 containsAll() 方法检查用户权限列表是否包含所有要求的权限。只有全部满足才算通过。
OR 逻辑:遍历所有要求的权限,只要用户拥有其中任意一个,就算通过。使用 break 提前退出循环以提高效率。
15.8.3. 使用示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| @RestController @RequestMapping("/document") public class DocumentController {
@GetMapping("/edit") @RequiresPermission(value = {"document:view", "document:edit"}, logical = RequiresPermission.LogicalOperator.AND) public String editDocument() { return "文档编辑成功"; }
@GetMapping("/delete") @RequiresPermission(value = {"document:delete", "admin:full"}, logical = RequiresPermission.LogicalOperator.OR) public String deleteDocument() { return "文档删除成功"; } }
|
第一个接口 editDocument 要求用户必须同时拥有 document:view 和 document:edit 两个权限才能访问。这适用于 “编辑前必须能查看” 的业务场景。
第二个接口 deleteDocument 要求用户拥有 document:delete 或 admin:full 其中任意一个权限即可。这适用于 “文档所有者或管理员都能删除” 的业务场景。
15.8.4. 本节小结
| 扩展点 | 原版 | 增强版 |
|---|
| 权限数量 | 单个 String | 多个 String[] |
| 逻辑关系 | 无 | AND / OR |
| 校验方式 | contains() | containsAll() / 遍历匹配 |
15.9. 本章总结与速查手册
15.9.1. RBAC 权限校验完整流程图
让我们用一张完整的流程图来总结整个 RBAC 权限校验系统的运行机制:

流程说明:
- 请求拦截:客户端发送 HTTP 请求,Spring MVC 的 DispatcherServlet 将请求路由到对应的 Controller 方法。
- AOP 介入:由于目标方法被
@RequiresPermission 标注,AOP 切面在方法执行前被触发。 - 身份识别:从请求头的
X-User-Id 中提取用户标识。 - 数据查询:通过两次数据库查询,先获取用户基本信息,再通过三表连接获取该用户的完整权限列表。
- 权限比对:将用户拥有的权限列表与注解要求的权限进行匹配。
- 决策执行:
- 如果权限不足,立即抛出异常,目标方法不会被执行
- 如果权限充足,放行请求,执行真正的业务逻辑
整个过程对业务代码完全透明,实现了权限控制的 “无感植入”。
15.9.2. AOP 核心注解速查表
| 知识点 | 说明 | 代码示例 |
|---|
| 切面声明 | 使用 @Aspect 和 @Component | @Aspect @Component public class MyAspect {} |
| 切点表达式 - execution | 按方法签名匹配 | @Before("execution(* com.example.service.*.*(..))") |
| 切点表达式 - @annotation | 按注解匹配 | @Before("@annotation(com.example.anno.Log)") |
| 前置通知 | 方法执行前 | @Before("pointcut()") |
| 后置通知 | 方法返回后 | @AfterReturning("pointcut()") |
| 环绕通知 | 完全控制方法执行 | @Around("pointcut()") Object around(ProceedingJoinPoint pjp) |
| 异常通知 | 方法抛异常时 | @AfterThrowing("pointcut()") |
| 最终通知 | 无论如何都执行 | @After("pointcut()") |
| 获取方法信息 | 通过 JoinPoint | joinPoint.getSignature().getName() |
| 获取注解实例 | 切点表达式注入 | @Before("@annotation(myAnno)") void before(MyAnno myAnno) |
| 控制方法执行 | 只能用 @Around | Object result = pjp.proceed(); |
15.9.3. 场景化代码模板
遇到以下场景时,请直接参考下方模板:
场景一:单权限校验
需求:某接口只有拥有 user:delete 权限的用户才能访问。
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
| @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface RequiresPermission { String value(); }
@Aspect @Component @RequiredArgsConstructor public class PermissionAspect { private final PermissionMapper permissionMapper;
@Before("@annotation(permission)") public void check(RequiresPermission permission) { Long userId = getCurrentUserId(); List<String> userPerms = permissionMapper.selectPermissionsByUserId(userId); if (!userPerms.contains(permission.value())) { throw new RuntimeException("403 Forbidden: 权限不足"); } } }
@DeleteMapping("/users/{id}") @RequiresPermission("user:delete") public void deleteUser(@PathVariable Long id) { }
|
场景二:多权限 AND 校验
需求:编辑文档需要同时拥有 doc:view 和 doc:edit 权限。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface RequiresPermission { String[] value(); LogicalOperator logical() default LogicalOperator.AND; enum LogicalOperator { AND, OR } }
if (logical == LogicalOperator.AND) { boolean pass = userPerms.containsAll(Arrays.asList(requiredPerms)); if (!pass) throw new RuntimeException("403 Forbidden"); }
@PutMapping("/docs/{id}") @RequiresPermission(value = {"doc:view", "doc:edit"}, logical = LogicalOperator.AND) public void editDoc(@PathVariable Long id) { }
|
场景三:多权限 OR 校验
需求:删除文档只需拥有 doc:delete 或 admin:full 其中之一即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| if (logical == LogicalOperator.OR) { boolean pass = false; for (String required : requiredPerms) { if (userPerms.contains(required)) { pass = true; break; } } if (!pass) throw new RuntimeException("403 Forbidden"); }
@DeleteMapping("/docs/{id}") @RequiresPermission(value = {"doc:delete", "admin:full"}, logical = LogicalOperator.OR) public void deleteDoc(@PathVariable Long id) { }
|
本章到此结束。我们从 RBAC 权限模型的理论基础出发,一步步构建了自定义注解、数据访问层、AOP 切面、全局异常处理等完整组件,最终实现了一套声明式的权限控制系统。通过这个实战项目,我们深刻体会到 AOP 在提升代码模块化、可维护性和复用性方面的巨大价值。