基于RBAC的权限控制:从表设计到接口实现全指南
基于RBAC的权限控制:从表设计到接口实现全指南
RBAC(Role-Based Access Control,基于角色的访问控制)是企业级应用权限管理的标准方案,核心通过“用户-角色-权限”三层关联,实现灵活、可扩展的权限管控。从核心概念切入,清晰展示表实体关系,再深入全局权限守卫和核心接口实现,并输出完整的API设计文档,让复杂权限逻辑更易理解与落地。
一、RBAC核心概念与优势
在动手前,先明确RBAC的三个核心实体及设计优势,理解“为什么要用RBAC”。
1. 三大核心实体
实体(Entity) | 定义与作用 | 关键关联 |
---|---|---|
用户(User) | 系统的操作者,通过关联角色获得权限 | 一个用户可关联多个角色(多对多) |
角色(Role) | 权限的集合(如“管理员”“普通员工”),定义用户的身份或职责 | 一个角色可关联多个用户/权限 |
权限(Permission) | 系统操作的最小单元(如“查看菜单”“删除数据”等) | 一个权限可关联多个角色 |
2. RBAC设计优势
- 扩展性:支持角色层级(如“部门经理”包含“组长”权限),适配复杂组织架构;
- 安全性:超管只需维护“角色-权限”关系,无需单独给用户赋权,减少操作失误;
- 可维护性:用户权限变更时,只需调整角色关联,无需逐个修改用户权限。
二、表实体设计与关系
RBAC的表设计需覆盖“用户-角色-权限”的关联,共6张核心表,以下用ER图展示关系,并附关键字段说明。
1. ER图(表关系可视化)
2. 核心表设计
表名 | 核心字段作用 | 设计亮点 |
---|---|---|
store_user | userType 区分管理员/普通用户,freezed 控制账号冻结状态 | 密码用salt 加盐加密,防明文泄露 |
store_permission | type 区分权限形态(菜单/按钮等),parentId 支持菜单层级(如“用户管理”下的子菜单) | 适配前端菜单渲染需求 |
store_permission_api | 关联“权限”与“接口”,实现“接口级权限控制”(如只有“管理员”能访问删除接口) | 精准控制接口访问范围 |
三、全局权限守卫:RoleAuthGuard
权限控制的核心是“拦截请求,校验权限”,通过NestJS的Guard
(守卫)实现全局拦截,确保每一次请求都经过权限校验。工作流程如下:
- 判断接口是否需要Token(如登录接口无需Token);
- 判断接口是否需要权限校验(如公开接口无需校验);
- 校验当前用户是否有访问该接口的权限。
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/current | GET | 无(从Token解析用户ID) | { code: 200, message: "success", data: { id: 1, username: "admin", roles: [{id:1}], permissions: [...] } } | 所有登录用户 |
获取用户列表 | /user/list | GET | Query参数:page : 页码(默认1)pageSize : 每页条数(默认10)username : 用户名(模糊查询,可选) | { code: 200, message: "success", data: { list: [...], total: 50, page: 1, pageSize: 10 } } | 管理员 |
更新用户信息 | /user | PUT | Body参数:id : 用户ID(必传)username : 用户名(可选)roleIds : 角色ID数组(可选)freezed : 是否冻结(可选) | { code: 200, message: "更新成功", data: { id: 1, username: "newAdmin" } } | 管理员(不能修改其他管理员) |
冻结/解冻用户 | /user/freezed | PUT | Body参数:id : 用户ID(必传)freezed : 0=解冻,1=冻结(必传) | { code: 200, message: "操作成功" } | 管理员(不能冻结自己/其他管理员) |
删除用户 | /user/:id | DELETE | Path参数:id : 用户ID(必传) | { code: 200, message: "删除成功" } | 管理员(不能删除管理员) |
3. 权限管理接口
接口名称 | 路径 | 方法 | 请求参数 | 响应示例 | 权限要求 |
---|---|---|---|---|---|
获取权限菜单 | /permission/menu | GET | 无(从Token解析用户ID) | { code: 200, message: "success", data: { list: [{ id: 1, title: "用户管理", children: [...] }] } } | 所有登录用户 |
获取接口权限列表 | /permission/api | GET | 无(从Token解析用户ID) | { code: 200, message: "success", data: [{ url: "/user/list", method: "GET" }, ...] } | 管理员(用于调试) |
4. 角色管理接口(补充)
接口名称 | 路径 | 方法 | 请求参数 | 响应示例 | 权限要求 |
---|---|---|---|---|---|
获取角色列表 | /role/list | GET | 无 | { code: 200, message: "success", data: [{ id: 1, name: "管理员" }, ...] } | 管理员 |
新增角色 | /role | POST | Body参数:name : 角色名称(必传)desc : 角色描述(可选)permissionIds : 权限ID数组(必传) | { code: 200, message: "新增成功", data: { id: 3, name: "运营" } } | 管理员 |
更新角色权限 | /role/permission | PUT | Body参数:roleId : 角色ID(必传)permissionIds : 权限ID数组(必传) | { code: 200, message: "更新成功" } | 管理员 |
六、总结
- 核心逻辑:RBAC的本质是“解耦用户与权限”,通过角色作为中间层,降低权限管理复杂度;全局守卫是权限控制的“守门人”,确保所有请求都经过权限校验;
- 关键优化点:
- 数据库查询:多用联表查询减少请求次数,用
JSON
函数处理一对多关系; - 缓存设计:用户信息、权限列表缓存到Redis,提升接口响应速度;
- 权限校验:严格区分管理员/普通用户权限,避免越权操作;
- 数据库查询:多用联表查询减少请求次数,用
- 思路:先理解“用户-角色-权限”表关系 → 实现全局守卫拦截逻辑 → 开发核心接口并优化 → 对接前端验证权限控制效果。