Skip to content
第 18 章 后端 ⏱ 13 分钟阅读

第 18 章:RBAC 权限 ​

学习目标 ​

  • 设计 RBAC 数据模型
  • 写 RolesGuard 和 @Roles 装饰器
  • 实现细粒度权限
  • 避开 3 个权限设计坑

一、RBAC 模型 ​

概念含义
User用户
Role角色(admin/user/guest)
Permission权限(user:create、user:delete)
UserRole用户 ↔ 角色
RolePermission角色 ↔ 权限
sql
users(id, username)
roles(id, name)
permissions(id, code)              -- 'user.create'
user_roles(user_id, role_id)
role_permissions(role_id, permission_id)

二、实体定义 ​

typescript
// role.entity.ts
@Entity()
export class Role {
  @PrimaryGeneratedColumn() id: number;
  @Column({ unique: true }) name: string;

  @ManyToMany(() => Permission)
  @JoinTable({ name: 'role_permissions' })
  permissions: Permission[];
}

// permission.entity.ts
@Entity()
export class Permission {
  @PrimaryGeneratedColumn() id: number;
  @Column({ unique: true }) code: string;     // 'user.create'
}

// user.entity.ts(加多对多)
@ManyToMany(() => Role)
@JoinTable({ name: 'user_roles' })
roles: Role[];

三、@Roles 装饰器 ​

typescript
// decorators/roles.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const ROLES_KEY = 'roles';
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);

四、RolesGuard ​

typescript
// guards/roles.guard.ts
@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private readonly reflector: Reflector) {}

  canActivate(ctx: ExecutionContext): boolean {
    const required = this.reflector.getAllAndOverride<string[]>(ROLES_KEY, [
      ctx.getHandler(), ctx.getClass(),
    ]);
    if (!required) return true;                       // 没标 @Roles 就放行

    const { user } = ctx.switchToHttp().getRequest();
    if (!user) throw new UnauthorizedException();

    const userRoles = (user.roles ?? []).map(r => r.name);
    return required.some(r => userRoles.includes(r));
  }
}

挂载:

typescript
@Controller('admin')
@UseGuards(JwtAuthGuard, RolesGuard)                // 先 JWT,再 RBAC
export class AdminController {
  @Get('users')
  @Roles('admin')
  list() { /* ... */ }
}

⚠️ 坑 1:Guard 顺序写反 → 未登录就能访问 admin,先 JwtAuthGuard 再 RolesGuard。

五、细粒度权限(@Permissions) ​

typescript
export const PERMISSIONS_KEY = 'permissions';
export const Permissions = (...codes: string[]) =>
  SetMetadata(PERMISSIONS_KEY, codes);

@Injectable()
export class PermissionsGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(ctx: ExecutionContext): boolean {
    const required = this.reflector.getAllAndOverride<string[]>(PERMISSIONS_KEY, [
      ctx.getHandler(), ctx.getClass(),
    ]);
    if (!required) return true;

    const { user } = ctx.switchToHttp().getRequest();
    const userPerms = (user.roles ?? []).flatMap(r =>
      (r.permissions ?? []).map(p => p.code),
    );
    return required.every(p => userPerms.includes(p));   // 必须全部满足
  }
}

用法:

typescript
@Post()
@Permissions('user.create')
create() {}

@Delete(':id')
@Permissions('user.delete')
remove() {}

六、初始化种子数据 ​

bash
pnpm add @nestjs/seed
typescript
// seeds/init.seed.ts
export default class InitSeed implements Seeder {
  async run(dataSource: DataSource): Promise<void> {
    // ① 拿 permissions 表的 Repository
    const permRepo = dataSource.getRepository(Permission);

    // ② 先建 3 条权限(基础数据,后面角色挂这些权限)
    await permRepo.save([
      { code: 'user.create' },  // 创建用户的权限
      { code: 'user.update' },  // 更新用户的权限
      { code: 'user.delete' },  // 删除用户的权限
    ]);

    // ③ 拿 roles 表的 Repository
    const roleRepo = dataSource.getRepository(Role);

    // ④ 创建 admin 角色(第 1 次 save,只插 name,permissions 还是空)
    const admin = await roleRepo.save({ name: 'admin' });

    // ⑤ 把刚建的 3 个权限全查出来,赋给 admin.permissions
    admin.permissions = await permRepo.find();

    // ⑥ 第 2 次 save:TypeORM 检测到 ManyToMany 字段变化,
    //    自动往中间表 role_permissions 写 3 条关联记录
    await roleRepo.save(admin);
  }
}

关键点:

步骤在干啥数据库变化
② permRepo.save([...])插 3 条权限permissions 表 +3 行
④ roleRepo.save({ name: 'admin' })插 1 个角色roles 表 +1 行,中间表还没数据
⑤ admin.permissions = ...内存赋值数据库还没变
⑥ roleRepo.save(admin)二次 saveTypeORM 检测到 ManyToMany 变化 → 自动写中间表

为什么必须 save 两次,不能一次搞定?

typescript
// ❌ 想一步到位?不行
const admin = await roleRepo.save({
  name: 'admin',
  permissions: [{ code: 'user.create' }, ...],   // 这种嵌套写法 TypeORM 不认
});
  • TypeORM 的 save() 对 ManyToMany 嵌套数组的处理不可靠(尤其跨实体时)
  • 稳妥做法:先建角色 → 再赋关系 → 再 save 一次(让中间表通过关系字段显式写入)

完整数据流向:

执行前:permissions=[], roles=[], role_permissions=[]

② 保存 → permissions: [user.create, user.update, user.delete]
④ 保存 → roles: [admin]                                 ⚠️ 中间表还空
⑤ 赋值 → admin.permissions = [3 个 Permission 实体]      (内存操作)
⑥ 保存 → role_permissions: [(admin, user.create), (admin, user.update), (admin, user.delete)]

执行后:admin 角色拥有全部 3 个权限 ✅

七、数据库查询带权限 ​

typescript
@Get()
list(@CurrentUser() user: any) {
  return this.repo.find({
    where: { id: In(user.ownedIds ?? []) },     // 看不到别人的
  });
}

⚠️ 坑 2:只做路由级权限,数据查询不限制 → 用户能看到别人的订单。

八、Ownership 资源归属 ​

典型场景:个人文章编辑/删除,只许本人操作自己的资源。

typescript
@Controller('articles')
export class ArticlesController {
  @Get(':id')                                article() {}                   // 公开
  @Put(':id')   @UseGuards(OwnershipGuard)   update() {}                    // 只能本人改自己
  @Delete(':id') @UseGuards(OwnershipGuard)  remove() {}                    // 只能本人删自己
}

对比前两个 Guard:

Guard解决什么判断依据
RolesGuard"你是 admin 吗?"角色字符串
PermissionsGuard"你会这个能力吗?"权限 code
OwnershipGuard"这资源是你的吗?"resource.userId === req.user.id
typescript
@Injectable()
export class OwnershipGuard implements CanActivate {
  async canActivate(ctx: ExecutionContext): Promise<boolean> {
    const req = ctx.switchToHttp().getRequest();
    const id = +req.params.id;
    const resource = await this.svc.findOne(id);

    if (resource.userId !== req.user.id) {
      throw new ForbiddenException();
    }
    return true;
  }
}

执行链路(用户 A 想删用户 B 的文章 id=101):

DELETE /articles/101
Authorization: Bearer <token: userId=1>
   ↓
const id = +req.params.id;                    // 101
const resource = await this.svc.findOne(101);  // { id:101, userId:2, ... }
if (resource.userId !== req.user.id) {         // 2 !== 1 → 命中
  throw new ForbiddenException();             // ❌ 403
}

完整 Guard 串联(角色权限矩阵里的 PUT /users/:id —— "admin 或本人"):

typescript
@Put(':id')
@UseGuards(JwtAuthGuard, RolesGuard, OwnershipGuard)
update() {}

// ① JwtAuthGuard       → 登录了吗?
// ② RolesGuard          → 是 admin?(放行)
//    或者不是 admin ↓
// ③ OwnershipGuard      → resource.userId === req.user.id?(放行)
//    否则 ❌ 403

为什么 RolesGuard 不够? 普通用户也能进 RolesGuard("user 角色"通常没 @Roles 限制),没 OwnershipGuard 就能改/删别人资源。

⚠️ 坑 3:Ownership 检查但没缓存 → 每个请求都查库,性能差,可加 Redis。

九、组合元数据 ​

typescript
// @Roles + @Permissions 组合用(装饰器层)
@Roles('admin')
@Permissions('user.delete')
@Delete(':id')
remove() {}

完整写法(Guard 层也要写上,跟装饰器配套):

typescript
@Controller('users')
@UseGuards(JwtAuthGuard, RolesGuard, PermissionsGuard, OwnershipGuard)
export class UsersController {
  @Delete(':id')
  @Roles('admin')                         // 装饰器 ① → RolesGuard 读
  @Permissions('user.delete')              // 装饰器 ② → PermissionsGuard 读
  remove() {}
}

两层的分工:

层级写在哪谁来读
装饰器层 @Roles(...) @Permissions(...)方法上RolesGuard / PermissionsGuard 用 Reflector 读
守卫层 @UseGuards(...)类上NestJS 按顺序执行

执行流程:

DELETE /users/123  Bearer <token>
   ↓
① JwtAuthGuard      → 验 token,挂 req.user
   ↓
② RolesGuard        → @Roles('admin') 在用户角色里?
   ↓
③ PermissionsGuard  → @Permissions('user.delete') 权限齐全?
   ↓
④ OwnershipGuard    → resource.userId === req.user.id?
   ↓
Controller.remove() → 删

完整 Guard 串联:JwtAuthGuard → RolesGuard → PermissionsGuard → OwnershipGuard。

十、实战:角色权限矩阵 ​

路由角色
GET /usersadmin, manager
POST /usersadmin
PUT /users/:idadmin 或本人
DELETE /users/:idadmin
typescript
@Controller('users')
@UseGuards(JwtAuthGuard, RolesGuard)
export class UsersController {
  @Get() @Roles('admin', 'manager') list() {}
  @Post() @Roles('admin') create() {}
  @Put(':id') @Roles('admin') update() {}
  @Delete(':id') @Roles('admin') remove() {}
}

十一、本章小结 ​

要点关键
模型User-Role-Permission 三层
GuardRolesGuard + Reflector.getAllAndOverride
装饰器@Roles(...names) + @Permissions(...codes)
顺序JwtAuthGuard → RolesGuard → PermissionsGuard
数据级路由权限 + 查询条件,不能只看路由
Ownership资源归属检查(本人或管理员)

动手练习 ​

  1. 角色 Guard:写 RolesGuard + @Roles('admin')
  2. 权限 Guard:写 PermissionsGuard + @Permissions('user.create')
  3. Ownership:写 OwnershipGuard,只允许本人修改自己的文章

下一章:第 19 章:Swagger 文档 →

本站基于 VitePress 构建 · 由 StackHub 团队维护