欢迎光临
我们一直在努力

基于 AOP+RBAC 的模拟 B 站权限管理系统

基于 AOP + RBAC 的模拟 B 站权限管理系统

目录

  • 项目背景
  • 核心架构设计
  • 搭建核心表结构
    • 表单设计
    • RBAC 模型原理
    • 建表 SQL
    • 实体类
  • 角色权限的分配与实现
  • 用户注册与登录
  • 自定义注解和切面
  • 接口类的简单示例
  • 取消某个角色的权限

项目背景

在类似 Bilibili 的内容社区中,用户身份错综复杂(游客、正式会员、硬核会员、大会员、UP主、管理员等)。不同的身份对应着差异化的功能权限,例如:

  • 低频校验:用户个人信息的修改。
  • 高频校验:视频观看(4K/1080P)、弹幕发送、动态发布。
  • 敏感操作:稿件审核、封禁用户。

本项目旨在通过 RBAC 模型 建立稳定的权限体系,并利用 AOP 技术 实现权限校验与业务逻辑的解耦,确保系统既安全又易于维护。

核心架构设计

项目采用典型的 RBAC (Role-Based Access Control) 模型,通过"用户-角色-权限"三层映射实现权限的灵活分配。

  • RBAC 模型实现:通过数据库五张表(用户表、角色表、权限表及两个中间表)构建底层数据结构。
  • AOP 切面拦截:利用 Spring AOP 前置通知(Before Advice)拦截所有标注了自定义注解的控制器方法。
  • 无感校验:业务开发人员只需在 Controller 方法上打个"标签",切面会自动提取当前登录态(Token),对比数据库/缓存中的权限集。

系统架构流程图

Pasted image 20260228131310|272

搭建核心表结构

表单设计

表单功能
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主”,也属于"管理员")。

Pasted image 20260224000213

建表 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);

编号角色 (Role)拥有权限 (Permissions)模拟场景
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 拥有上传权限

用户注册与登录

注册逻辑

用户在第一次注册时,会被赋予一些默认的角色:

  • 正式会员(USER)
  • UP主(可选)
  • 注册后,该用户拥有的角色记录会记录在 sys_user_role 中间表中,用联合索引做记录。

    登录逻辑

    在以后的登录中,初始时都会进行一次联表查询,通过角色表和两张中间表,查询用户拥有的权限,并将查询到的权限用 Redis Set 存储起来,注意设置过期时间。

    权限加载流程

    Pasted image 20260225002504

    代码实现

    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 权限校验时序图

    Pasted image 20260225002400

    接口类的简单示例

    我们在观看 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 个用户加减权限时漏掉某个人
    逻辑清晰 符合现实世界的组织架构 员工离职或调岗,只需更换他的"角色",权限自动跟随切换
    赞(0)
    未经允许不得转载:171主机测评 » 基于 AOP+RBAC 的模拟 B 站权限管理系统
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址