基于 AOP + RBAC 的模拟 B 站权限管理系统
目录
- 项目背景
- 核心架构设计
- 搭建核心表结构
- 表单设计
- RBAC 模型原理
- 建表 SQL
- 实体类
- 角色权限的分配与实现
- 用户注册与登录
- 自定义注解和切面
- 接口类的简单示例
- 取消某个角色的权限
项目背景
在类似 Bilibili 的内容社区中,用户身份错综复杂(游客、正式会员、硬核会员、大会员、UP主、管理员等)。不同的身份对应着差异化的功能权限,例如:
- 低频校验:用户个人信息的修改。
- 高频校验:视频观看(4K/1080P)、弹幕发送、动态发布。
- 敏感操作:稿件审核、封禁用户。
本项目旨在通过 RBAC 模型 建立稳定的权限体系,并利用 AOP 技术 实现权限校验与业务逻辑的解耦,确保系统既安全又易于维护。
核心架构设计
项目采用典型的 RBAC (Role-Based Access Control) 模型,通过"用户-角色-权限"三层映射实现权限的灵活分配。
- RBAC 模型实现:通过数据库五张表(用户表、角色表、权限表及两个中间表)构建底层数据结构。
- AOP 切面拦截:利用 Spring AOP 前置通知(Before Advice)拦截所有标注了自定义注解的控制器方法。
- 无感校验:业务开发人员只需在 Controller 方法上打个"标签",切面会自动提取当前登录态(Token),对比数据库/缓存中的权限集。
系统架构流程图

搭建核心表结构
表单设计
| sys_user(用户表) | 存储注册的用户数据 |
| sys_role(角色表) | 存储不同的角色 |
| sys_permission(权限表) | 存储不同的权限 |
| sys_user_role(用户—角色表) | 中间表,将用户和角色串联起来 |
| sys_role_permission(角色—权限表) | 中间表,将角色和权限串联起来 |
RBAC 模型原理
1. 为什么要引入"角色(Role)"?(解耦)
场景: 假如 B 站现在有 100 万个"正式会员"。
-
如果不设角色:你需要在 100 万个用户身上逐一标注"可发弹幕"、“可投币”。如果哪天 B 站修改规则,正式会员不能投币了,你需要修改 100 万行数据。
-
引入角色后:100 万个用户只关联一个"正式会员"角色。你只需要在权限关联表里,把"正式会员"对应的"投币权限"删掉,这 100 万人的权限瞬间同步修改。
结论: 角色是权限的"容器",它把人和权限内容隔离开,极大地降低了维护成本。
2. 为什么需要两张"中间表"?(多对多关系)
数据库设计中有个原则:当 A 和 B 是多对多关系时,必须建立中间表。
-
用户与角色(多对多):
- 一个用户可以有多个角色(比如:你既是"大会员",又是"UP主")。
- 一个角色可以被多个用户拥有(比如:有几千万个"普通会员")。
-
角色与权限(多对多):
- 一个角色有多个权限("UP主"可以上传视频、删除自己视频的评论)。
- 一个权限可以属于多个角色(“上传视频"权限既属于"UP主”,也属于"管理员")。

建表 SQL
— 1. 用户表 (User)
CREATE TABLE `sys_user` (
`id` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
`username` VARCHAR(50) NOT NULL UNIQUE COMMENT '用户名/手机号',
`password` VARCHAR(100) NOT NULL COMMENT '密码',
`nickname` VARCHAR(50) COMMENT '昵称',
`user_type` TINYINT DEFAULT 0 COMMENT '0:普通用户, 1:UP主, 2:管理员'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
— 2. 角色表 (Role)
CREATE TABLE `sys_role` (
`id` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
`role_name` VARCHAR(50) NOT NULL COMMENT '角色名称:如正式会员、大会员',
`role_key` VARCHAR(50) NOT NULL UNIQUE COMMENT '角色权限标识:如COMMON_USER, BIG_VIP'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
— 3. 权限表 (Permission)
CREATE TABLE `sys_permission` (
`id` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
`perm_name` VARCHAR(50) NOT NULL COMMENT '权限描述:如发弹幕',
`perm_tag` VARCHAR(50) NOT NULL UNIQUE COMMENT '权限标识:如danmu:send'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
— 4. 用户-角色关联表 (User-Role Mapping)
CREATE TABLE `sys_user_role` (
`user_id` BIGINT NOT NULL,
`role_id` BIGINT NOT NULL,
`expire_time` DATETIME DEFAULT NULL COMMENT '角色过期时间,NULL表示永久有效',
PRIMARY KEY (`user_id`, `role_id`),
INDEX `idx_role_id` (`role_id`) — 优化反向查询
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
— 5. 角色-权限关联表 (Role-Permission Mapping)
CREATE TABLE `sys_role_permission` (
`role_id` BIGINT NOT NULL,
`permission_id` BIGINT NOT NULL,
PRIMARY KEY (`role_id`, `permission_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
实体类
Java 中对应的实体类:
| User | userId、userName、userNickName、userPassword |
| Role | roleId、roleName、roleKey |
| Permission | permissionId、permName、permTag |
| UserRole | userId、roleId、expireTime(过期时间) |
| RolePermission | roleId、permissionId |
角色权限的分配与实现
我们需要事先约定好角色和权限之间的关系,并在 role 表和 permission 表中插入角色和权限的数据:
— 插入角色
INSERT INTO sys_role (role_name, role_key) VALUES
('游客', 'GUEST'),
('正式会员', 'USER'),
('大会员', 'VIP'),
('UP主', 'UP');
— 插入权限
INSERT INTO sys_permission (perm_name, perm_tag) VALUES
('观看视频', 'video:view'),
('发送弹幕', 'danmu:send'),
('观看4K视频', 'video:4k'),
('上传视频', 'video:upload'),
('删除视频', 'video:delete'),
('搜索视频', 'video:search');
— 角色与权限关联
— GUEST 只能看视频
INSERT INTO sys_role_permission VALUES (1, 1);
— USER 可以看视频、发弹幕
INSERT INTO sys_role_permission VALUES (2, 1), (2, 2);
— VIP 可以看视频、发弹幕、看4K
INSERT INTO sys_role_permission VALUES (3, 1), (3, 2), (3, 3);
— UP 可以看视频、发弹幕、上传视频
INSERT INTO sys_role_permission VALUES (4, 1), (4, 2), (4, 4);
| 1 | GUEST (游客) | video:view | 只能看普通视频 |
| 2 | USER (正式会员) | video:view, danmu:send | 能看视频、发弹幕 |
| 3 | VIP (大会员) | video:view, danmu:send, video:4k | 额外享受 4K 画质 |
| 4 | UP (UP主) | video:view, danmu:send, video:upload | 拥有上传权限 |
用户注册与登录
注册逻辑
用户在第一次注册时,会被赋予一些默认的角色:
注册后,该用户拥有的角色记录会记录在 sys_user_role 中间表中,用联合索引做记录。
登录逻辑
在以后的登录中,初始时都会进行一次联表查询,通过角色表和两张中间表,查询用户拥有的权限,并将查询到的权限用 Redis Set 存储起来,注意设置过期时间。
权限加载流程

代码实现
package com.minrbac.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.minrbac.common.RoleEnum;
import com.minrbac.common.constants.RedisConstant;
import com.minrbac.common.utils.RedisUtils;
import com.minrbac.entity.SysUser;
import com.minrbac.entity.SysUserRole;
import com.minrbac.mapper.SysPermissionMapper;
import com.minrbac.mapper.SysUserMapper;
import com.minrbac.mapper.SysUserRoleMapper;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.Date;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/**
* 用户业务逻辑
*/
@Service
public class UserService {
@Resource
private SysUserMapper sysUserMapper;
@Resource
private SysUserRoleMapper sysUserRoleMapper;
@Resource
private SysPermissionMapper sysPermissionMapper;
@Resource
private RedisUtils<String> redisUtils;
/**
* 注册并分配默认角色
*/
public void register(String username, String password, String nickname) {
// 检查用户名是否存在
SysUser existingUser = sysUserMapper.selectOne(new LambdaQueryWrapper<SysUser>()
.eq(SysUser::getUsername, username));
if (existingUser != null) {
throw new RuntimeException("用户名已存在");
}
// 2. 保存用户
SysUser user = new SysUser();
user.setUsername(username);
user.setPassword(password);
user.setNickname(nickname);
// 除了细分的角色外,还需要大致区分一下当前用户负责的职责,0表示普通用户,1表示管理员用户
user.setUserType(0);
sysUserMapper.insert(user);
// 3. 分配默认角色,我们这里使用的枚举类
SysUserRole userRole = new SysUserRole();
userRole.setUserId(user.getId());
userRole.setRoleId(RoleEnum.USER.getRoleType());
sysUserRoleMapper.insert(userRole);
}
/**
* 登录
*
* @return 返回 Token
*/
public String login(String username, String password) {
// 1. 简单校验用户(实际生产环境需要加密匹配)
SysUser user = sysUserMapper.selectOne(new LambdaQueryWrapper<SysUser>()
.eq(SysUser::getUsername, username)
.eq(SysUser::getPassword, password));
if (user == null) {
throw new RuntimeException("用户名或密码错误");
}
// 2. 生成 Token (简单 UUID)
String token = UUID.randomUUID().toString();
// 3. 缓存用户信息到 Redis
String tokenKey = RedisConstant.AUTH_TOKEN_PREFIX + token;
redisUtils.set(tokenKey, user.getId().toString(), RedisConstant.TOKEN_EXPIRE_TIME, TimeUnit.HOURS);
// 4. 加载并缓存权限列表
loadPermissionsToCache(user.getId());
return token;
}
/**
* 用户充值方法
*
* @param userId 用户ID
* @param amount 充值金额
* @return 充值结果
*/
public String recharge(Integer userId, Double amount) {
// 验证用户是否存在
SysUser user = sysUserMapper.selectById(userId);
if (user == null) {
throw new RuntimeException("用户不存在");
}
// 模拟充值逻辑
System.out.println("用户 " + user.getUsername() + " 充值金额: " + amount + " 元");
// 赋予大会员角色
assignBigVipRole(userId);
// 重新加载用户权限到缓存
loadPermissionsToCache(userId);
return "充值成功,已为您开通大会员权限!";
}
/**
* 为用户分配大会员角色
*
* @param userId 用户ID
*/
private void assignBigVipRole(Integer userId) {
// 1. 检查用户是否已有大会员角色
SysUserRole existingRole = sysUserRoleMapper.selectOne(new LambdaQueryWrapper<SysUserRole>()
.eq(SysUserRole::getUserId, userId)
.eq(SysUserRole::getRoleId, RoleEnum.BIG_VIP.getRoleType()));
if (existingRole != null) {
// 如果已有大会员角色,更新过期时间(延长30天)
existingRole.setExpireTime(new Date(System.currentTimeMillis() + 30L * 24 * 60 * 60 * 1000));
sysUserRoleMapper.updateById(existingRole);
} else {
// 如果没有大会员角色,新增角色关联
SysUserRole userRole = new SysUserRole();
userRole.setUserId(userId);
userRole.setRoleId(RoleEnum.BIG_VIP.getRoleType());
// 设置大会员权限有效期为30天
userRole.setExpireTime(new Date(System.currentTimeMillis() + 30L * 24 * 60 * 60 * 1000));
sysUserRoleMapper.insert(userRole);
}
System.out.println("用户ID " + userId + " 已获得大会员角色");
}
/**
* 加载权限到 Redis
*/
public void loadPermissionsToCache(Integer userId) {
// 联表查询出该用户的所有权限
List<String> perms = sysPermissionMapper.selectPermTagsByUserId(userId);
String permsKey = RedisConstant.AUTH_PERMS_PREFIX + userId;
// 先删除旧内容
redisUtils.del(permsKey);
if (perms != null && !perms.isEmpty()) {
// 这里使用 Redis 的 Set 存储当前用户拥有的所有权限
for (String perm : perms) {
redisUtils.sSet(permsKey, perm);
}
redisUtils.expire(permsKey, RedisConstant.PERMS_EXPIRE_TIME, TimeUnit.HOURS);
}
}
}
MyBatis 查询语句
<select id="selectPermTagsByUserId" resultType="java.lang.String">
SELECT DISTINCT p.perm_tag
FROM sys_permission p
JOIN sys_role_permission rp ON p.id = rp.permission_id
JOIN sys_user_role ur ON rp.role_id = ur.role_id
WHERE ur.user_id = #{userId}
AND (ur.expire_time IS NULL OR ur.expire_time > NOW())
</select>
查询出来的数据(permTag)会被储存在 Redis 中,设置一个过期时间(比用户登录过期时间长一点),用户进行某些操作时,鉴权系统会从 Redis 中获取,将需要的权限与用户拥有权限进行对比。
自定义注解和切面
在用户进行某些操作时,前端调用接口时,不用再简单地 Service 层中进行复杂的鉴权判断,而是通过 AOP 进行鉴权,AOP 不通过就直接进行拦截,更加高效。
自定义注解
给注解的属性,是每个操作需要的权限,比如删除视频需要 video:delete、观看4K视频需要 video:4k。
package com.minrbac.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 {
/**
* 需要的权限标识,比如 "video:delete"
*/
String value() default "";
}
AOP 切面
我们从注解获取该方法所需要的权限,再通过 userId 获取该用户拥有的权限,进行对比,如果有该权限,就放行,否则进行拦截。
package com.minrbac.aspect;
import com.minrbac.annotation.RequiresPermission;
import com.minrbac.common.constants.RedisConstant;
import com.minrbac.common.utils.RedisUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
/**
* 权限校验切面
*/
@Aspect
@Component
public class AuthAspect {
@Resource
private RedisUtils<String> redisUtils;
@Before("@annotation(com.minrbac.annotation.RequiresPermission)")
public void doBefore(JoinPoint point) {
// 获取注解上给接口类的权限标识
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
RequiresPermission annotation = method.getAnnotation(RequiresPermission.class);
String requiredPerm = annotation.value();
// 如果该接口类方法不需要权限,就直接放行
if (!StringUtils.hasText(requiredPerm)) {
return;
}
// 从 Header 中获取 Token
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes == null) {
throw new RuntimeException("系统异常,无法获得权限");
}
HttpServletRequest request = attributes.getRequest();
String token = request.getHeader("Authorization");
if (!StringUtils.hasText(token)) {
throw new RuntimeException("未登录,请先登录");
}
String userId = (String) redisUtils.get(RedisConstant.AUTH_TOKEN_PREFIX + token);
if (!StringUtils.hasText(userId)) {
throw new RuntimeException("登录失效,请重新登录");
}
// 校验用户是否拥有当前权限
String permKey = RedisConstant.AUTH_PERMS_PREFIX + userId;
boolean hasPerm = redisUtils.sHasKey(permKey, requiredPerm);
// 权限校验不通过
if (!hasPerm) {
throw new RuntimeException("当前用户无权限,缺少权限:" + requiredPerm);
}
}
}
AOP 权限校验时序图

接口类的简单示例
我们在观看 4K 视频时,就需要 video:4k 权限,这时在控制类方法上加一个注解,添加所需权限属性就可以了,而不需要在 Service 层中写复杂的鉴权判别。
package com.minrbac.controller;
import com.minrbac.annotation.RequiresPermission;
import com.minrbac.service.VideoService;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
/**
* 视频管理控制器
*/
@RestController
@RequestMapping("/video")
public class VideoController {
@Resource
private VideoService videoService;
/**
* 观看4K视频接口
* 需要 video:watch4k 权限
*/
@GetMapping("/watch4k/{videoId}")
@RequiresPermission("video:watch4k")
public String watch4KVideo(@PathVariable String videoId) {
return videoService.watch4KVideo(videoId);
}
/**
* 删除视频接口
* 需要 video:delete 权限
*/
@DeleteMapping("/delete/{videoId}")
@RequiresPermission("video:delete")
public String deleteVideo(@PathVariable String videoId) {
return videoService.deleteVideo(videoId);
}
/**
* 上传视频接口
* 需要 video:upload 权限
*/
@PostMapping("/upload")
@RequiresPermission("video:upload")
public String uploadVideo(String title, String file) {
return videoService.uploadVideo(title, file);
}
/**
* 搜索视频接口
* 需要 video:search 权限
*/
@GetMapping("/search")
@RequiresPermission("video:search")
public String searchVideo(String keyword) {
return videoService.searchVideo(keyword);
}
/**
* 获取视频详情(公开接口,无需特殊权限)
*/
@GetMapping("/detail/{videoId}")
public String getVideoDetail(@PathVariable String videoId) {
return "视频详情: " + videoId + " (公开访问)";
}
}
取消某个角色的权限
取消某个角色的权限,在数据库层面其实就是操作 sys_role_permission 关联表。
我们通常直接从关联表中删除对应的记录:
— 取消 ID 为 1 的角色拥有的 ID 为 10 的权限
DELETE FROM sys_role_permission
WHERE role_id = 1 AND permission_id = 10;
为什么要通过"角色"取消,而不是直接从"用户"身上取消?
这就是 RBAC 模型的精髓,这样做有以下显著好处:
| 解耦与高效 | 无需遍历成千上万的用户。只需修改 1 个角色,所有关联用户立即生效 | 某视频平台下线"投币"功能,只需操作一次角色权限 |
| 降低出错率 | 权限管理变得模块化。你只需关注"大会员能干什么",而不是"张三能干什么" | 避免因手动给 100 个用户加减权限时漏掉某个人 |
| 逻辑清晰 | 符合现实世界的组织架构 | 员工离职或调岗,只需更换他的"角色",权限自动跟随切换 |





