当前位置: 首页 > news >正文

基于RBAC的权限控制:从表设计到接口实现全指南

基于RBAC的权限控制:从表设计到接口实现全指南

RBAC(Role-Based Access Control,基于角色的访问控制)是企业级应用权限管理的标准方案,核心通过“用户-角色-权限”三层关联,实现灵活、可扩展的权限管控。从核心概念切入,清晰展示表实体关系,再深入全局权限守卫核心接口实现,并输出完整的API设计文档,让复杂权限逻辑更易理解与落地。

一、RBAC核心概念与优势

在动手前,先明确RBAC的三个核心实体及设计优势,理解“为什么要用RBAC”。

1. 三大核心实体

实体(Entity)定义与作用关键关联
用户(User)系统的操作者,通过关联角色获得权限一个用户可关联多个角色(多对多)
角色(Role)权限的集合(如“管理员”“普通员工”),定义用户的身份或职责一个角色可关联多个用户/权限
权限(Permission)系统操作的最小单元(如“查看菜单”“删除数据”等)一个权限可关联多个角色

2. RBAC设计优势

  • 扩展性:支持角色层级(如“部门经理”包含“组长”权限),适配复杂组织架构;
  • 安全性:超管只需维护“角色-权限”关系,无需单独给用户赋权,减少操作失误;
  • 可维护性:用户权限变更时,只需调整角色关联,无需逐个修改用户权限。

二、表实体设计与关系

RBAC的表设计需覆盖“用户-角色-权限”的关联,共6张核心表,以下用ER图展示关系,并附关键字段说明。

1. ER图(表关系可视化)

USERUSER_ROLEROLEROLE_PERMISSIONPERMISSIONPERMISSION_API关联被关联关联被关联关联

2. 核心表设计

表名核心字段作用设计亮点
store_useruserType区分管理员/普通用户,freezed控制账号冻结状态密码用salt加盐加密,防明文泄露
store_permissiontype区分权限形态(菜单/按钮等),parentId支持菜单层级(如“用户管理”下的子菜单)适配前端菜单渲染需求
store_permission_api关联“权限”与“接口”,实现“接口级权限控制”(如只有“管理员”能访问删除接口)精准控制接口访问范围

三、全局权限守卫:RoleAuthGuard

权限控制的核心是“拦截请求,校验权限”,通过NestJS的Guard(守卫)实现全局拦截,确保每一次请求都经过权限校验。工作流程如下:

  1. 判断接口是否需要Token(如登录接口无需Token);
  2. 判断接口是否需要权限校验(如公开接口无需校验);
  3. 校验当前用户是否有访问该接口的权限。

1. 核心代码实现

// src/auth/role-auth.guard.ts
import { CanActivate, ExecutionContext, Injectable, ForbiddenException } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { pathToRegexp } from 'path-to-regexp'; // 用于匹配接口路径
import { ALLOW_NO_PERMISSION } from 'src/common/decorators/permission.decorator'; // 免权限装饰器
import { PermissionService } from 'src/permission/permission.service';
import { ALLOW_NO_TOKEN } from 'src/common/decorators/token.decorator'; // 免Token装饰器
import { UserType } from 'src/common/enums/common.enum'; // 用户类型枚举@Injectable()
export class RoleAuthGuard implements CanActivate {constructor(private readonly reflector: Reflector, // 用于读取接口装饰器配置private readonly permissionService: PermissionService, // 权限查询服务) {}async canActivate(ctx: ExecutionContext): Promise<boolean> {// 1. 免Token校验(如登录接口)const allowNoToken = this.reflector.getAllAndOverride<boolean>(ALLOW_NO_TOKEN,[ctx.getHandler(), ctx.getClass()], // 优先方法装饰器,再类装饰器);if (allowNoToken) return true;// 2. 免权限校验(如公开接口)const allowNoPerm = this.reflector.getAllAndOverride<boolean>(ALLOW_NO_PERMISSION,[ctx.getHandler(), ctx.getClass()],);if (allowNoPerm) return true;// 3. 获取当前用户(JWT守卫已将用户信息注入req.user)const req = ctx.switchToHttp().getRequest();const user = req.user;if (!user) throw new ForbiddenException('请先登录');// 4. 管理员拥有所有权限,直接放行if (user.userType === UserType.ADMIN_USER) return true;// 5. 校验接口权限:获取用户拥有的所有接口权限,匹配当前请求const userApis = await this.permissionService.getPermApiList(user); // 查用户的接口权限列表const currentUrl = req.url.split('?')[0]; // 去除URL参数(如?id=1)const currentMethod = req.method.toUpperCase(); // 统一方法大写(如GET/POST)// 匹配规则:方法一致 + 路径匹配(支持动态路径,如/api/user/:id)const hasPermission = userApis.some(permApi => {return permApi.method.toUpperCase() === currentMethod && pathToRegexp(permApi.url).test(currentUrl.replace('/api', '')); // 适配接口路径前缀});if (!hasPermission) throw new ForbiddenException('您无权限访问该接口');return true;}
}

2. 全局注册守卫

app.module.ts中注册,实现所有接口的自动拦截:

// src/app.module.ts
import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';
import { JwtAuthGuard } from './auth/jwt-auth.guard';
import { RoleAuthGuard } from './auth/role-auth.guard';@Module({providers: [{ provide: APP_GUARD, useClass: JwtAuthGuard }, // jwt登录态校验{ provide: APP_GUARD, useClass: RoleAuthGuard }, // 接口权限校验],
})
export class AppModule {}

四、核心接口实现(带优化说明)

RBAC 权限控制的核心接口围绕“用户管理”“权限查询”展开,以下实现关键接口,并标注设计亮点与优化点。

1. 接口1:获取当前用户信息(含角色+权限)

功能:用户登录后,获取自身基本信息、关联角色及权限(用于前端渲染菜单/按钮)。

// src/user/user.service.tsasync getCurrentUser(currentUser: UserEntity) {// 1. 联表查询:用户 -> 用户角色 -> 角色 -> 角色权限 -> 权限(一次获取所有关联数据)const rawData = await this.dataSource.createQueryBuilder().select(['user.id AS userId','user.username AS userName','role.id AS roleId','role.name AS roleName','p.id AS permId','p.title AS permTitle','p.type AS permType','p.code AS permCode',]).from('store_user', 'user').leftJoin('store_user_role', 'ur', 'user.id = ur.userId').leftJoin('store_role', 'role', 'ur.roleId = role.id').leftJoin('store_role_permission', 'rp', 'role.id = rp.roleId').leftJoin('store_permission', 'p', 'rp.permissionId = p.id').where('user.id = :userId', { userId: currentUser.id }).orderBy('p.parentId', 'ASC') // 按父权限排序,方便前端转树形结构.getRawMany();// 2. 数据格式化:去重角色,整理权限const roles = Array.from(new Set(rawData.map(item => item.roleId))).map(id => ({ id }));const permissions = rawData.map(item => ({id: item.permId,title: item.permTitle,type: item.permType,code: item.permCode,parentId: item.permParentId, // 补充父权限ID,用于树形菜单}));// 3. 过滤敏感字段(密码、盐值)const { password, salt, ...safeUserInfo } = currentUser;return {...safeUserInfo,roles, // 用户关联的角色列表permissions, // 用户拥有的权限列表(前端可据此渲染菜单/按钮)};}

优化点

  • 联表查询一次获取所有关联数据,减少数据库请求,提升效率;
  • 去重角色:“用户 - 角色 - 权限” 一对多关系,用Set 对查询结果去重;(避免一条权限对应一条角色记录);
  • 过滤敏感字段(password/salt),防止信息泄露。

2. 接口2:获取用户列表(带分页+模糊查询+角色)

功能:管理员查询用户列表,支持按用户名模糊搜索、分页,并返回每个用户的关联角色。

// src/user/user.service.tsasync getUserList(dto: UserListDto) {const { username, page = 1, pageSize = 10 } = dto;const skip = (page - 1) * pageSize; // 计算分页偏移量// 1. 构建查询:联表用户与角色,用JSON函数生成角色数组(避免重复记录)let query = this.dataSource.createQueryBuilder('store_user', 'u').leftJoin('store_user_role', 'ur', 'u.id = ur.userId').leftJoin('store_role', 'r', 'ur.roleId = r.id').select(['u.*',// 生成JSON格式的角色列表(避免一条角色对应一条用户记录)"JSON_ARRAYAGG(JSON_OBJECT('id', r.id, 'name', r.name)) AS roles",]).groupBy('u.id') // 按用户ID分组,确保一个用户一条记录.skip(skip).take(pageSize);// 2. 模糊查询:用户名匹配if (username) {query = query.where('u.username LIKE :username', { username: `%${username}%` });}// 3. 执行查询并处理结果const [list, total] = await Promise.all([query.getRawMany(),this.countUserTotal(username), // 单独计算总数,优化性能]);// 4. 过滤敏感字段,返回安全数据const safeList = list.map(user => {Reflect.deleteProperty(user, 'password');Reflect.deleteProperty(user, 'salt');user.roles = user.roles ? JSON.parse(user.roles) : []; // JSON字符串转数组return user;});return { list: safeList, total, page, pageSize };}// 计算用户总数(用于分页)private async countUserTotal(username?: string) {let query = this.dataSource.createQueryBuilder('store_user', 'u').select('COUNT(u.id)', 'total');if (username) {query = query.where('u.username LIKE :username', { username: `%${username}%` });}const { total } = await query.getRawOne();return Number(total);}

优化点

  • JSON_ARRAYAGG+JSON_OBJECT生成角色数组,避免“用户-角色”一对多导致的重复记录;
  • 分页用skip+take,总数单独查询(减少联表计算压力),而非用getManyAndCount;

3. 接口3:更新用户信息(含角色调整)

功能:修改用户基本信息,并同步更新用户关联的角色(需权限校验:普通用户不能修改管理员)。

// src/user/user.service.ts
async update(updateUserDto: UpdateUserDto, currentUser: UserEntity) {const { id, roleIds, ...otherParams } = updateUserDto;// 1. 校验用户是否存在const user = await this.userRepository.findOne({ where: { id } });if (!user) throw new HttpException('用户不存在', HttpStatus.NOT_FOUND);// 2. 权限校验:普通用户不能修改管理员信息if (user.userType === UserType.ADMIN_USER && currentUser.userType === UserType.NORMAL_USER) {throw new HttpException('无权限修改管理员信息', HttpStatus.FORBIDDEN);}// 3. 更新用户基本信息(避免直接覆盖,用plainToClass确保实体字段匹配)const updatedUser = plainToClass(UserEntity,{ ...user, ...otherParams },{ ignoreDecorators: true } // 忽略TypeORM装饰器,仅映射字段);const { password, salt, ...safeUser } = await this.userRepository.save(updatedUser);// 4. 同步更新用户角色(先删后加,确保角色关系最新)if (roleIds && roleIds.length > 0) {// 4.1 删除旧角色关联await this.userRoleRepository.delete({ userId: id });// 4.2 批量添加新角色关联const userRoles = roleIds.map(roleId => ({ userId: id, roleId }));const saveResult = await this.userRoleRepository.save(userRoles);if (saveResult.length !== roleIds.length) {throw new HttpException('角色更新失败,请重试', HttpStatus.EXPECTATION_FAILED);}}// 5. 刷新Redis缓存(避免缓存脏数据)const redisKey = getRedisKey(RedisKeyPrefix.USER_INFO, id);await this.redisService.hSet(redisKey, safeUser); // 存储非敏感信息return { message: '更新成功', data: safeUser };
}

优化点

  • 权限校验严格:普通用户无法修改管理员,避免越权操作;
  • 角色更新采用“先删后加”:确保旧角色完全移除,新角色准确生效;
  • 缓存同步:更新后立即刷新 Redis,保证后续请求获取最新数据。

4. 接口4:冻结/解冻用户

功能:管理员可冻结 / 解冻用户账号(冻结后用户无法登录),需严格权限校验(不能冻结自己或其他管理员)。

```ts
async updateFreezedStatus(id: number, freezed: number, currUserId: number) {// 1. 用户不能冻结自己if (id === currUserId) {throw new HttpException('你不能冻结自己', HttpStatus.EXPECTATION_FAILED);}// 2. 判断用户是否存在const user = await this.userRepository.findOne({ where: { id } });if (!user) {throw new HttpException('用户不存在', HttpStatus.EXPECTATION_FAILED);}// 3. 禁止冻结管理员if (user.userType === UserType.ADMIN_USER) {throw new HttpException('你没有权限修改管理员信息', HttpStatus.FORBIDDEN);}// 4. 更新数据const { affected } = await this.userRepository.update({ id }, { freezed });if (!affected) {throw new HttpException(`${freezed ? '冻结' : '解冻'}失败,请稍后重试`,HttpStatus.EXPECTATION_FAILED,);}// 5. 更新redis缓存const redisKey = getRedisKey(RedisKeyPrefix.USER_INFO, id);const { password, ...rest } = user;await this.redisService.hSet(redisKey, classToPlain({ ...rest, freezed }));return '操作成功';
}
```

优化点

  • 权限校验:禁止用户冻结自己账号,禁止冻结管理员账号(userType=0)
  • 数据一致性:更新数据库后立即同步 Redis 缓存,操作结果实时生效(冻结后用户登录会失败)
  • 异常处理:用户不存在时返回 404,操作失败时明确提示(如 “冻结失败,请重试”)

5. 接口5:删除用户

功能:管理员删除用户,同时清理用户与角色的关联关系及缓存(不能删除管理员)。

```ts
async delete(id: number) {// 1. 判断用户是否存在const user = await this.userRepository.findOne({ where: { id } });if (!user) {throw new HttpException('用户不存在', HttpStatus.EXPECTATION_FAILED);}// 2. 禁止删除管理员if (user.userType === UserType.ADMIN_USER) {throw new HttpException('你没有权限删除管理员', HttpStatus.FORBIDDEN);}// 3. 开始事务:确保用户删除与角色关联清理的原子性const { affected } = await this.userRepository.delete({ id });if (!affected) {throw new HttpException('删除失败,请稍后重试',HttpStatus.EXPECTATION_FAILED,);}// 4. 删除角色关联表const result = await this.userRoleRepository.delete({ userId: id });if (!result) {throw new HttpException('删除失败,请稍后重试',HttpStatus.EXPECTATION_FAILED,);}// 5. 删除redis缓存const redisKey = getRedisKey(RedisKeyPrefix.USER_INFO, id);await this.redisService.del(redisKey);return '删除成功';
}
```

优化点

  • 数据完整性:删除用户时同步删除user_role关联表记录,避免残留无效的角色关联数据
  • 权限控制:严格禁止删除管理员账号,确保系统核心管理员账户安全
  • 缓存清理:彻底删除 Redis 中该用户的缓存信息,避免后续请求读取到已删除用户的缓存数据

6. 接口6:获取用户权限菜单(树形结构)

功能:根据当前用户角色,返回树形结构的权限菜单(用于前端渲染侧边栏菜单)。

// src/permission/permission.service.tsasync getPermMenuList(currentUser: UserEntity) {let permissions: PermissionEntity[];// 1. 管理员获取所有权限,普通用户获取关联权限if (currentUser.userType === UserType.ADMIN_USER) {permissions = await this.permissionRepo.find({order: { parentId: 'ASC', id: 'ASC' }, // 按父ID和自身ID排序,确保树形结构正确});} else {// 联表查询:用户 -> 用户角色 -> 角色权限 -> 权限permissions = await this.dataSource.createQueryBuilder().select('p.*').from('store_user_role', 'ur').leftJoin('store_role_permission', 'rp', 'ur.roleId = rp.roleId').leftJoin('store_permission', 'p', 'rp.permissionId = p.id').where('ur.userId = :userId', { userId: currentUser.id }).groupBy('p.id') // 去重权限(一个用户可能多角色对应同一权限).orderBy('p.parentId', 'ASC').getRawMany<PermissionEntity>();}// 2. 列表转树形结构const menuTree = listToTree(permissions, { root: 0, pidKey: 'parentId', childrenKey: 'children' });return { list: menuTree };}

优化点

  • 区分管理员/普通用户权限范围:管理员获全量权限,普通用户获关联权限;
  • 列表转树形:用工具函数统一处理层级关系,适配前端菜单渲染需求;
  • 接口权限去重:避免多角色对应同一接口导致的重复校验。

五、完整API接口设计文档

以下是基于RBAC的核心API设计,包含接口路径、请求参数、响应格式及权限要求,可直接用于前后端对接。

1. 基础信息

项目说明
基础路径/api
认证方式JWT(请求头携带Authorization: Bearer {token}
统一响应格式{ code: number, message: string, data?: any }

2. 用户管理接口

接口名称路径方法请求参数响应示例权限要求
获取当前用户信息/user/currentGET无(从Token解析用户ID){ code: 200, message: "success", data: { id: 1, username: "admin", roles: [{id:1}], permissions: [...] } }所有登录用户
获取用户列表/user/listGETQuery参数:
page: 页码(默认1)
pageSize: 每页条数(默认10)
username: 用户名(模糊查询,可选)
{ code: 200, message: "success", data: { list: [...], total: 50, page: 1, pageSize: 10 } }管理员
更新用户信息/userPUTBody参数:
id: 用户ID(必传)
username: 用户名(可选)
roleIds: 角色ID数组(可选)
freezed: 是否冻结(可选)
{ code: 200, message: "更新成功", data: { id: 1, username: "newAdmin" } }管理员(不能修改其他管理员)
冻结/解冻用户/user/freezedPUTBody参数:
id: 用户ID(必传)
freezed: 0=解冻,1=冻结(必传)
{ code: 200, message: "操作成功" }管理员(不能冻结自己/其他管理员)
删除用户/user/:idDELETEPath参数:
id: 用户ID(必传)
{ code: 200, message: "删除成功" }管理员(不能删除管理员)

3. 权限管理接口

接口名称路径方法请求参数响应示例权限要求
获取权限菜单/permission/menuGET无(从Token解析用户ID){ code: 200, message: "success", data: { list: [{ id: 1, title: "用户管理", children: [...] }] } }所有登录用户
获取接口权限列表/permission/apiGET无(从Token解析用户ID){ code: 200, message: "success", data: [{ url: "/user/list", method: "GET" }, ...] }管理员(用于调试)

4. 角色管理接口(补充)

接口名称路径方法请求参数响应示例权限要求
获取角色列表/role/listGET{ code: 200, message: "success", data: [{ id: 1, name: "管理员" }, ...] }管理员
新增角色/rolePOSTBody参数:
name: 角色名称(必传)
desc: 角色描述(可选)
permissionIds: 权限ID数组(必传)
{ code: 200, message: "新增成功", data: { id: 3, name: "运营" } }管理员
更新角色权限/role/permissionPUTBody参数:
roleId: 角色ID(必传)
permissionIds: 权限ID数组(必传)
{ code: 200, message: "更新成功" }管理员

六、总结

  1. 核心逻辑:RBAC的本质是“解耦用户与权限”,通过角色作为中间层,降低权限管理复杂度;全局守卫是权限控制的“守门人”,确保所有请求都经过权限校验;
  2. 关键优化点
    • 数据库查询:多用联表查询减少请求次数,用JSON函数处理一对多关系;
    • 缓存设计:用户信息、权限列表缓存到Redis,提升接口响应速度;
    • 权限校验:严格区分管理员/普通用户权限,避免越权操作;
  3. 思路:先理解“用户-角色-权限”表关系 → 实现全局守卫拦截逻辑 → 开发核心接口并优化 → 对接前端验证权限控制效果。
http://www.dtcms.com/a/346187.html

相关文章:

  • Beszel 服务器监控平台使用教程
  • JVM虚拟机
  • Leetcode—1683. 无效的推文【简单】
  • 网络与信息安全有哪些岗位:(7)等级保护测评师
  • tensorflow-gpu 2.7下的tensorboard与profiler插件版本问题
  • 第九章 Leaflet 实战:多边形绘制工具开发与面积实时计算(含双击报错修复方案)
  • Qt QML实现 无边框圆角窗口拖动(附窗口控制按钮)
  • RAG初筛方案实例验证-多种BM25方案
  • 类器官培养基系列,助力高效医学研究
  • Navicat连接MySQL-出现1045无法连接问题
  • AI实验管理神器:WandB全功能解析
  • 【python】os.mkdir() 和 os.makedirs()区别
  • 数学建模-灰色关联分析
  • map_set
  • Trie 树(字典树)
  • Rust 入门 注释和文档之 cargo doc (二十三)
  • 51单片机-中断系统
  • 【数据分享】各省及全国GDP增长指数(1980-2022)
  • 彻底解决 Windows 文件扩展名隐藏问题,注册表修改显示文件后缀方法
  • More Effective C++ 条款01:仔细区别 pointers 和 references
  • 构建城市数字孪生底座:深度解析智慧城市全景视频拼接融合解决方案
  • constraint_mode使用
  • 【Python】两条命令永久切国内源
  • Android 16环境开发的一些记录
  • C语言中的CSI_START和CSI_END宏
  • 拿到手一个前端项目,应该如何启动
  • 多目标跟踪中基于目标威胁度评估的传感器控制方法复现
  • lanczos算法学习笔记
  • 【GM3568JHF】FPGA+ARM异构开发板 测试命令
  • OFD格式文件及Python将PDF转换为OFD格式文件