
RBAC-基于角色的访问控制(Role-Based Access Control)是一种广泛使用的权限管理模型,它通过角色分配权限,用户通过角色获得权限。RBAC 模型的核心概念包括 用户(User)、角色(Role) 和 权限(Permission)。这种模型简化了权限管理,使得系统管理员能够更高效地管理用户权限。
以下是对 RBAC 模型的详细解释,以及如何在实际系统中实现 RBAC 用户权限控制。
RBAC 的核心概念
1.1 用户(User)
用户是系统中的一个实体,可以是一个人或其他需要访问系统资源的主体。用户通过登录系统获得身份验证,并根据其角色获得相应的权限。
1.2 角色(Role)
角色是一组权限的集合,代表了用户在系统中的职责或身份。例如:
- 管理员(Admin):拥有系统管理权限。
- 普通用户(User):拥有基本的读取权限。
- 编辑(Editor):拥有内容编辑权限。
1.3 权限(Permission)
权限是用户可以执行的操作或访问的资源。例如:
- 读取权限(Read):允许用户查看数据。
- 写入权限(Write):允许用户修改数据。
- 删除权限(Delete):允许用户删除数据。
1.4 用户 ↔ 角色 ↔ 权限
- 用户与角色的关系:一个用户可以拥有多个角色,一个角色也可以被多个用户拥有。
- 角色与权限的关系:一个角色可以包含多个权限,一个权限也可以被多个角色拥有。
RBAC 的实现方式
在实际系统中,RBAC 的实现通常包括以下几个步骤:
2.1 数据模型设计
设计用户、角色和权限的数据模型,并建立它们之间的关系。
表:Users
- 用户ID
- 用户名
- 密码
- 等等...
表:Roles
- 角色ID
- 角色名称
- 等等...
表:Permissions
- 权限ID
- 权限名称
- 等等...
表:UserRoles
- 用户ID
- 角色ID
表:RolePermissions
- 角色ID
- 权限ID
2.2 角色分配
为用户分配角色。这通常通过用户管理界面完成,管理员可以将角色分配给用户。
2.3 权限分配
为角色分配权限。这通常通过角色管理界面完成,管理员可以将权限分配给角色。
2.4 权限检查
在用户执行操作时,系统需要检查用户是否拥有相应的权限。这通常通过以下步骤完成:
- 获取用户的角色。
- 获取角色的权限。
- 检查用户是否拥有执行操作所需的权限。
数据库设计
一、整体架构
该模型通过角色(Role)作为中介,将用户(User)与权限(Permission)解耦,实现细粒度的权限管理。
表介绍
| 表名 | 核心作用 |
|
| 存储用户基本信息 |
|
| 定义系统角色 |
|
| 用户与角色的多对多映射 |
|
| 定义最小操作单元的权限(如:按钮) |
|
| 角色与元素操作的授权关系 |
|
| 定义系统导航菜单的访问权限 |
|
| 角色与页面菜单的授权关系 |
表关系模型图

表关系文字说明
- 用户表(USER):存储用户的基本信息。
- 角色表(ROLE):定义系统中的角色。
- 用户-角色关联表(USER_ROLE):表示用户与角色的多对多关系,一个用户可以拥有多个角色,一个角色可以被多个用户拥有。
- 元素操作权限表(ELEMENT_OPERATION):定义最小操作单元的权限,如:按钮点击等。

- 角色-元素操作关联表(ROLE_ELEMENT_OPERATION):定义角色与元素操作的授权关系。
- 页面菜单权限表(PAGE_MENU):定义系统导航菜单的访问权限,如:某个新页面。
-
- up 主视角

-
- 粉丝视角

-
- 访客视角

注释:UP 主、粉丝、访客看到的菜单选项(收藏、追番追剧、设置等)、页面按钮(已关注、关注、发消息等)是不同的。
- 角色-页面菜单关联表(ROLE_PAGE_MENU):定义角色与页面菜单的授权关系。
建表语句
- 角色表(ROLE):定义系统中的角色。
DROP TABLE IF EXISTS `auth_role`;
CREATE TABLE `auth_role` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`name` VARCHAR(255) DEFAULT NULL COMMENT '角色名称',
`code` VARCHAR(50) NOT NULL COMMENT '角色唯一编码',
`createTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `uk_code` (`code`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 COMMENT='角色表';
- 用户-角色关联表(USER_ROLE):表示用户与角色的多对多关系,一个用户可以拥有多个角色,一个角色可以被多个用户拥有。
DROP TABLE IF EXISTS `user_role`;
CREATE TABLE `user_role` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`userId` BIGINT NOT NULL COMMENT '用户ID',
`roleId` BIGINT NOT NULL COMMENT '角色ID',
`createTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_userId` (`userId`),
KEY `idx_roleId` (`roleId`)
) ENGINE=InnoDB AUTO_INCREMENT=1 COMMENT='用户角色关联表';
- 元素操作权限表(ELEMENT_OPERATION):定义最小操作单元的权限,如:按钮点击等。
DROP TABLE IF EXISTS `auth_element_operation`;
CREATE TABLE `auth_element_operation` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`elementName` VARCHAR(255) DEFAULT NULL COMMENT '页面元素名称',
`elementCode` VARCHAR(50) DEFAULT NULL COMMENT '页面元素唯一编码',
`operationType` VARCHAR(5) DEFAULT NULL COMMENT '操作类型:0可点击 1可见',
`createTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 COMMENT='权限控制--页面元素操作表';
- 角色-元素操作关联表(ROLE_ELEMENT_OPERATION):定义角色与元素操作的授权关系。
DROP TABLE IF EXISTS `auth_role_element_operation`;
CREATE TABLE `auth_role_element_operation` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`roleId` BIGINT NOT NULL COMMENT '角色ID',
`elementOperationId` BIGINT NOT NULL COMMENT '元素操作ID',
`createTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_roleId` (`roleId`),
KEY `idx_elementOperationId` (`elementOperationId`)
) ENGINE=InnoDB AUTO_INCREMENT=1 COMMENT='权限控制--角色与元素操作关联表';
- 页面菜单权限表(PAGE_MENU):定义系统导航菜单的访问权限,如:某个新页面。
DROP TABLE IF EXISTS `auth_menu`;
CREATE TABLE `auth_menu` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`name` VARCHAR(255) DEFAULT NULL COMMENT '菜单项目名称',
`code` VARCHAR(50) DEFAULT NULL COMMENT '唯一编码',
`createTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 COMMENT='权限控制-页面访问表';
- 角色-页面菜单关联表(ROLE_PAGE_MENU):定义角色与页面菜单的授权关系。
DROP TABLE IF EXISTS `auth_role_menu`;
CREATE TABLE `auth_role_menu` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`roleId` BIGINT NOT NULL COMMENT '角色ID',
`menuId` BIGINT NOT NULL COMMENT '页面菜单ID',
`createTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_roleId` (`roleId`),
KEY `idx_menuId` (`menuId`)
) ENGINE=InnoDB AUTO_INCREMENT=1 COMMENT='权限控制--角色页面菜单关联表';
二、权限控制流程

- 用户登录:验证账号密码(结合盐值加密)。
- 角色加载:根据用户关联的角色列表,加载所有角色。
- 权限校验:
-
- 页面级:检查角色是否有权访问当前页面(通过
auth_role_page_menu)。 - 元素级:检查角色是否有权执行特定操作(通过
auth_role_elemenoperation)。
- 页面级:检查角色是否有权访问当前页面(通过
- 动态拦截:前端/后端根据权限标记隐藏或禁用操作按钮。
三、设计亮点
- 细粒度控制:
-
- 既能控制页面访问(如 "商品页"),也能控制页面内的具体操作(如 "删除按钮")。
- 灵活扩展:
-
- 新增角色或权限时,仅需修改关联表,无需改动核心表结构。
- 安全性:
-
- 密码存储使用盐值,避免明文泄露风险。
- 易维护性:
-
- 通过角色实现权限批量分配,降低管理成本。
四、潜在优化点
- 权限缓存:增加缓存层(如Redis),减少实时查询压力。
- 预加载策略:用户首次登录时预加载全部权限,提升后续操作响应速度。
- 审计日志:记录权限变更历史(如谁在何时给角色添加了哪些权限)。
前端展示权限控制:元素操作(如:视频投稿)、页面菜单(如:购买邀请码)
后端将用户拥有的权限下发给前端,前端基于权限码,控制前端页面的展示。如:是否展示视频投稿按钮,是否展示购买邀请码这个菜单选项。
初始化一些数据
- 为用户 1 赋予 p5 的角色,p5 的角色可以点击发布视频。
- 为用户 2 赋予 p9 的角色,p9 的角色可以看见购买邀请码。
1.1. 初始化角色表

1.2. 初始化页面元素操作表

1.3. 为角色赋予权限


表明:LV1 用户拥有视频投稿这个功能,即可点击。
1.4. 为用户赋予个角色

1.5. 初始化页面菜单表:一般是跳转到一个新的页面

1.6. 为角色赋予权限

1.7. 为用户赋予角色

接口开发:查询用户权限
1.1.1. 登录获取用户 token

1.1.2. 查询用户权限
接口地址:http://localhost:8888/api/user-auth

用户 1:
{
"code": "0",
"data": {
"roleElementOperationList": [
{
"authElementOperation": {
"createTime": null,
"elementCode": "VIDEO_POST_BUTTON",
"elementName": "视频投稿按钮",
"id": 1,
"operationType": "0",
"updateTime": null
},
"createTime": "2025-03-10 19:33:31",
"elementOperationId": 1,
"roleId": 1
}
],
"roleMenuList": []
},
"msg": "成功"
}
用户 2:
{
"code": "0",
"data": {
"roleElementOperationList": [],
"roleMenuList": [
{
"authMenu": {
"code": "PurchaseInvitationCode",
"createTime": null,
"id": 1,
"name": "购买邀请码",
"updateTime": null
},
"createTime": "2025-03-10 19:38:32",
"menuId": 1,
"roleId": 5
}
]
},
"msg": "成功"
}
后端需要控制的权限:接口、数据权限控制
仿制恶意用户绕过前端代码直接访问数据,前后端结合的权限控制使我们的数据更加安全。
1. Spring AOP切面编程
什么是 AOP?
AOP(Aspect-Oriented Programming)是一种编程范式,用于在不修改原有代码的情况下,为程序添加额外的功能。比如,你可以在方法执行前后添加日志记录、性能监控、事务管理等功能。通过使用Spring AOP,你可以创建更干净、模块化、易于维护的代码,同时让系统变得更加灵活和可扩展。
AOP中的几个关键概念
- 连接点(Join Point):程序执行过程中的某个点,比如方法的调用。在Spring AOP中,连接点通常是方法的执行。
- 切点(Pointcut):定义了哪些连接点是我们感兴趣的,也就是哪些方法需要被拦截。比如,你可以定义一个切点,拦截所有以
save开头的方法。切点定义了哪些方法需要被拦截。Spring AOP使用AspectJ的表达式语法来定义切点。 - 通知(Advice):当切点被触发时,要执行的代码。比如,在方法执行前打印日志、在方法执行后记录结果等。
-
- 前置通知(Before Advice):在目标方法执行之前执行。
- 后置通知(After Returning Advice):在目标方法成功执行之后执行。
- 异常通知(After Throwing Advice):在目标方法抛出异常时执行。
- 最终通知(After Finally Advice):无论目标方法是否成功,都会执行。
- 环绕通知(Around Advice):可以完全控制目标方法的执行,可以在执行前后插入代码。
- 切面(Aspect):把切点和通知组合在一起,定义了一个完整的AOP功能。比如,一个切面可以是“在所有
save方法执行前打印日志”。切面类是定义AOP功能的核心。我们需要用@Aspect注解标记一个类为切面类,并用@Component或@Configuration将其注册为Spring的Bean。
2. 实例:日志记录切面
这段代码的核心目标是通过**面向切面编程(AOP)**的方式,为指定的方法添加日志记录功能。具体来说,它会在方法执行前后分别记录日志,而不需要在方法内部手动编写日志代码。
1 自定义注解:LoggingMonitor
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface LoggingMonitor {
}
- 作用:这是一个自定义注解,用于标记需要被监控的方法。
@Retention(RetentionPolicy.RUNTIME):表示这个注解在运行时仍然有效,这样切面类可以在运行时通过反射获取到它。@Target(ElementType.METHOD):表示这个注解只能应用于方法上。
2 服务类:MyService
@Service
public class MyService {
@LoggingMonitor
public void doSomething() {
System.out.println("Doing something in MyService");
}
}
- 作用:这是一个普通的业务服务类,其中有一个方法
doSomething()。 @Service:这是Spring框架中的注解,表示这是一个服务类,会被Spring容器管理。@LoggingMonitor:这个注解标记了doSomething()方法,表示这个方法需要被日志切面监控。
3 切面类:LoggingAspect
@Aspect
@Component
public class LoggingAspect {
@Pointcut("@annotation(org.example.service.annotation.LoggingMonitor)")
public void serviceMethods() {
// 空实现,不需要任何逻辑
}
@Before("serviceMethods()")
public void logBefore() {
System.out.println("Method is about to be executed");
}
@After("serviceMethods()")
public void logAfter() {
System.out.println("Method has been executed");
}
}
- 作用:切面类用于定义在方法执行前后要做的事情(比如日志记录)。
@Aspect:这是AOP的核心注解,表示这是一个切面类。@Component:表示这个类会被Spring容器管理。@Pointcut:定义了一个切入点,表示哪些方法会被切面类监控。这里的切入点是所有被@LoggingMonitor注解标记的方法。@Before:定义了一个前置通知,表示在方法执行之前执行的逻辑。@After:定义了一个后置通知,表示在方法执行之后执行的逻辑。
4. 运行流程梳理
4.1 方法调用触发
当MyService.doSomething()方法被调用时,Spring AOP框架会检测到这个方法上有@LoggingMonitor注解。
4.2 前置通知(logBefore)执行
- 切面类
LoggingAspect中的@Before注解标记的logBefore()方法会被触发。 - 执行
logBefore()方法中的逻辑,打印日志:
Method is about to be executed
4.3 原始方法执行
- 接下来,
MyService.doSomething()方法本身会被执行。 - 打印:
Doing something in MyService
4.4 后置通知(logAfter)执行
- 切面类
LoggingAspect中的@After注解标记的logAfter()方法会被触发。 - 执行
logAfter()方法中的逻辑,打印日志:
Method has been executed
通过AOP技术,我们可以在不修改原始业务逻辑代码的情况下,为方法添加额外的功能(如日志记录)。具体来说:
- 自定义注解
@LoggingMonitor标记需要监控的方法。 - 切面类
LoggingAspect通过@Pointcut定义监控的切入点。 - 使用
@Before和@After注解分别定义方法执行前后的行为。 - 当被标记的方法被调用时,AOP框架会自动触发切面类中的逻辑。
5. 接口 测试

输出结果:

3. 接口权限控制
ApiLimitedAspect
功能:等级为 P5 的用户不能发布动态消息,即不能调用发布动态那个接口。
设计思路
这段代码的设计思路是“黑名单”模式,即:
- 使用
@ApiLimited注解标记接口方法,并指定一组受限角色。 - 如果用户拥有这些受限角色之一,则禁止访问该接口。
这种设计思路适用于一些特殊场景,例如:
- 某些接口只允许非管理员访问。
- 某些接口只允许非黑名单用户访问。
扩展:如果限制角色(黑名单数量)越来越多怎么办呢?会造成我们的注解十分庞大
- 可以引入”角色组“
- 动态加载受限角色
如果受限角色列表可能动态变化,可以将受限角色存储在数据库或配置中心中,而不是硬编码在注解里。例如:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public @interface ApiLimitedRole {
String roleKey() default ""; // 动态角色键
}
然后在切面中动态加载受限角色:
@Before("check() && @annotation(apiLimitedRole)")
public void doBefore(JoinPoint joinPoint, ApiLimitedRole apiLimitedRole) {
Long userId = userSupport.getCurrentUserId();
List<UserRole> userRoleList = userRoleService.getUserRoleByUserId(userId);
String roleKey = apiLimitedRole.roleKey();
String[] limitedRoleCodeList = roleConfigService.getLimitedRoles(roleKey); // 动态加载受限角色
...
}
接口测试
接口地址:http://localhost:8888/api/userMoment
输出结果:

4. 数据权限控制
DataLimitedAspect
字段取值的限制,可能不同的角色要求的字段值不同。如:p9 用户只能发布视频类型得内容。
优化建议
动态配置限制规则
目前的限制规则是硬编码的(ROLE_P9和type == "0")。如果未来需要支持更复杂的规则,可以将规则存储在配置文件或数据库中,动态加载。例如:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public @interface DataLimited {
String roleKey() default ""; // 动态角色键
String typeValue() default ""; // 限制的类型值
}
然后在切面中动态加载规则:
@Before("check() && @annotation(dataLimited)")
public void doBefore(JoinPoint joinPoint, DataLimited dataLimited) {
Long userId = userSupport.getCurrentUserId();
List<UserRole> userRoleList = userRoleService.getUserRoleByUserId(userId);
Set<String> roleCodeSet = userRoleList.stream().map(UserRole::getRoleCode).collect(Collectors.toSet());
String requiredRole = dataLimited.roleKey();
String requiredType = dataLimited.typeValue();
Object[] args = joinPoint.getArgs();
for (Object arg : args) {
if (arg instanceof UserMoment) {
UserMoment userMoment = (UserMoment) arg;
if (roleCodeSet.contains(requiredRole) && !requiredType.equals(userMoment.getType())) {
log.warn("User [{}] is denied access to method [{}] due to data limitation: Role [{}] requires type [{}]",
userId, joinPoint.getSignature().getName(), requiredRole, requiredType);
throw new ConditionException("参数异常");
}
}
}
}
接口测试

9644

被折叠的 条评论
为什么被折叠?



