【Nest】权限管理——RBAC/CASL
一、RBAC vs CASL(对比与适用场景)
RBAC(Role-Based Access Control)
- 思路:给用户分配角色(如
admin
,editor
,viewer
),再给角色分配权限(或直接在角色层面判断)。 - 优点:易管理、便于审计、适合权限较少、粒度较粗的场景。
- 缺点:无法灵活表达对象级别或属性级别的细粒度规则(比如“用户可以编辑自己发布的文章”)除非在代码里加额外判断。
CASL(能力/能力表述,Capability-based)
- 思路:用“能力”(Ability)来表达“谁能对哪个资源做什么(action + subject)”,可以非常细粒度(支持条件、字段限制、对象级判断)。
- 优点:灵活,支持条件/属性级授权(例如:
can('update', 'Article', { authorId: user.id })
)。 - 缺点:比RBAC复杂,需要把能力从角色或其他来源构造出来(通常在 AbilityFactory 中基于用户角色/权限或其他属性动态构造 Ability)。
常见组合方式
- 用 RBAC 管理“粗粒度”的模块级/页面级权限(如是否能进入管理页面),用 CASL 做资源级/字段级的细粒度检查(如是否能编辑 / 删除某条数据)。
- 或者把“角色”映射到 CASL 能力(AbilityFactory 从角色生成 ability)。
二、数据库模型(关系型,示例使用 PostgreSQL / MySQL,配合 TypeORM)
下面给出表结构与关系(ER 概念),以及 TypeORM 实体草稿。
ER 概要
users
(1) — (M)user_roles
— (M)roles
roles
(1) — (M)role_permissions
— (M)permissions
- 也可直接
users
->user_permissions
(当需要给用户直接授予特定 permission) - 资源表示例:
articles
(用于演示对象级权限)
SQL 表定义(简化版)
-- users
CREATE TABLE users (id BIGINT PRIMARY KEY AUTO_INCREMENT,username VARCHAR(100) NOT NULL UNIQUE,email VARCHAR(255) UNIQUE,password_hash VARCHAR(255) NOT NULL,is_active BOOLEAN DEFAULT TRUE,created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);-- roles
CREATE TABLE roles (id BIGINT PRIMARY KEY AUTO_INCREMENT,name VARCHAR(50) NOT NULL UNIQUE,description TEXT
);-- permissions
CREATE TABLE permissions (id BIGINT PRIMARY KEY AUTO_INCREMENT,name VARCHAR(100) NOT NULL UNIQUE, -- e.g., 'article.create', 'article.update'description TEXT
);-- user_roles (many-to-many)
CREATE TABLE user_roles (user_id BIGINT NOT NULL,role_id BIGINT NOT NULL,PRIMARY KEY (user_id, role_id)
);-- role_permissions (many-to-many)
CREATE TABLE role_permissions (role_id BIGINT NOT NULL,permission_id BIGINT NOT NULL,PRIMARY KEY (role_id, permission_id)
);-- 也可以直接对用户授予权限
CREATE TABLE user_permissions (user_id BIGINT NOT NULL,permission_id BIGINT NOT NULL,PRIMARY KEY (user_id, permission_id)
);-- Example resource: articles
CREATE TABLE articles (id BIGINT PRIMARY KEY AUTO_INCREMENT,title VARCHAR(255),content TEXT,author_id BIGINT,created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
TypeORM 实体(简化示例)
下面只展示关键字段与关系,省略装饰器配置的细节(但足够用于理解):
// user.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, ManyToMany, JoinTable } from 'typeorm';
import { Role } from './role.entity';@Entity('users')
export class User {@PrimaryGeneratedColumn()id: number;@Column({ unique: true })username: string;@Column()passwordHash: string;@ManyToMany(() => Role, role => role.users)@JoinTable({ name: 'user_roles', joinColumn: { name: 'user_id' }, inverseJoinColumn: { name: 'role_id' } })roles: Role[];// 可选:直接 permissions// @ManyToMany(() => Permission)// @JoinTable({ name: 'user_permissions' })// permissions: Permission[];
}// role.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, ManyToMany } from 'typeorm';
import { User } from './user.entity';
import { Permission } from './permission.entity';@Entity('roles')
export class Role {@PrimaryGeneratedColumn()id: number;@Column({ unique: true })name: string;@ManyToMany(() => User, user => user.roles)users: User[];@ManyToMany(() => Permission, perm => perm.roles)@JoinTable({ name: 'role_permissions', joinColumn: { name: 'role_id' }, inverseJoinColumn: { name: 'permission_id' } })permissions: Permission[];
}// permission.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, ManyToMany } from 'typeorm';
import { Role } from './role.entity';@Entity('permissions')
export class Permission {@PrimaryGeneratedColumn()id: number;@Column({ unique: true })name: string; // 'article.create' etc.@ManyToMany(() => Role, role => role.permissions)roles: Role[];
}// article.entity.ts (用于演示对象级控制)
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';@Entity('articles')
export class Article {@PrimaryGeneratedColumn()id: number;@Column()title: string;@Column('text')content: string;@Column()authorId: number;
}
三、RBAC:@Roles()
装饰器 + RolesGuard
(基于 Guard 的实现)
这是比较常见的实现:在 Controller/Route 上写 @Roles('admin','editor')
,Guard 从 request.user
读取角色并核对。
1) 自定义装饰器 @Roles()
// roles.decorator.ts
import { SetMetadata } from '@nestjs/common';export const ROLES_KEY = 'roles';
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);
2) RolesGuard
// roles.guard.ts
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ROLES_KEY } from './roles.decorator';@Injectable()
export class RolesGuard implements CanActivate {constructor(private reflector: Reflector) {}async canActivate(context: ExecutionContext): Promise<boolean> {// 使用 UseGuards的类,内部方法使用 @Roles 装饰器// 则 getAllAndOverride 在方法的角色会将外层类的角色覆盖,如果使用 getAllAndMerge 则会合并两个角色const requiredRoles = this.reflector.getAllAndOverride<string[]>(ROLES_KEY, [context.getHandler(),context.getClass(),]);if (!requiredRoles || requiredRoles.length === 0) {// 没声明角色,则默认允许(或改为默认 deny)return true;}const request = context.switchToHttp().getRequest();const user = request.user;if (!user) {throw new ForbiddenException('用户未登录或未找到 user');}// user.roles 需在 auth 中间件/策略里注入(例如 JWT 验证后查到用户并附带角色)const userRoles: string[] = (user.roles || []).map((r: any) => (typeof r === 'string' ? r : r.name));const has = requiredRoles.some(role => userRoles.includes(role));if (!has) {throw new ForbiddenException('权限不足');}return true;}
}
3) 使用方式(Controller 示例)
import { Controller, Get, UseGuards } from '@nestjs/common';
import { Roles } from './roles.decorator';
import { RolesGuard } from './roles.guard';
import { JwtAuthGuard } from './jwt-auth.guard'; // 假设你有 jwt guard@UseGuards(JwtAuthGuard, RolesGuard)
@Controller('admin')
export class AdminController {@Roles('admin')@Get('stats')getStats() {return { ok: true, stats: {} };}
}
注意:
JwtAuthGuard
应该把request.user
填充为包含roles
的对象(通常在 JWT payload 或在 Guard/Strategy 里从 DB 查询填充)。- RolesGuard 适合“角色集合匹配”的场景,但无法表达“只能修改自己的帖子”这种对象级权限。
四、CASL(基于能力的授权)实现(在 NestJS 中常见模式)
下面展示一个基于 @casl/ability
(v5+)的实现:AbilityFactory、@CheckAbilities()
装饰器与 PoliciesGuard
。Ability 可以基于用户的 roles/permissions 动态构建。
需要安装:
npm i @casl/ability @casl/ability-for-nestjs
(这里给出通用实现,不依赖 casl 的 Nest 插件)
1) Ability Types
// ability.factory.ts
import { Injectable } from '@nestjs/common';
import { Ability, AbilityBuilder, AbilityClass, ExtractSubjectType, InferSubjects } from '@casl/ability';
import { Article } from './entities/article.entity';// 定义 Action 和 Subjects
export type Actions = 'manage' | 'create' | 'read' | 'update' | 'delete';
export type Subjects = InferSubjects<typeof Article> | 'Article' | 'all';export type AppAbility = Ability<[Actions, Subjects]>;@Injectable()
export class AbilityFactory {createForUser(user: any) {const { can, cannot, build } = new AbilityBuilder<Ability<[Actions, Subjects]>>(Ability as AbilityClass<AppAbility>);if (!user) {// 未认证用户,默认只读公开资源can('read', 'Article');return build();}// 基于角色或 permissions 构造 abilityconst roles = (user.roles || []).map(r => (typeof r === 'string' ? r : r.name));if (roles.includes('admin')) {can('manage', 'all'); // admin 管理所有return build();}// 基于角色到能力的映射(可扩展)if (roles.includes('editor')) {can('create', 'Article');can('read', 'Article');can('update', 'Article');}// 示例:如果用户是文章作者,则允许 update/delete 自己的文章can('update', 'Article', { authorId: user.id });can('delete', 'Article', { authorId: user.id });// 若有 permissions 字段也可遍历赋权if (user.permissions) {for (const perm of user.permissions) {// 假设 perm.name: 'article.create'const [subject, action] = perm.name.split('.');can(action as Actions, subject[0].toUpperCase() + subject.slice(1));}}return build({// 需要暴露 subjectType 的检测方式detectSubjectType: item => (typeof item === 'string' ? item : (item.constructor as ExtractSubjectType<any>)),});}
}
2) 自定义装饰器 @CheckAbilities()
// check-abilities.decorator.ts
import { SetMetadata } from '@nestjs/common';export const CHECK_ABILITY = 'check_ability';
export const CheckAbilities = (...requirements: Array<{ action: string; subject: any }>) =>SetMetadata(CHECK_ABILITY, requirements);
3) PoliciesGuard(读取 Ability 并检查)
// policies.guard.ts
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { CHECK_ABILITY } from './check-abilities.decorator';
import { AbilityFactory } from './ability.factory';
import { AppAbility } from './ability.factory';
import { ForbiddenError } from '@casl/ability';@Injectable()
export class PoliciesGuard implements CanActivate {constructor(private reflector: Reflector, private abilityFactory: AbilityFactory) {}canActivate(context: ExecutionContext): boolean {const requirements = this.reflector.getAllAndOverride<Array<{ action: string; subject: any }>>(CHECK_ABILITY, [context.getHandler(),context.getClass(),]);if (!requirements || requirements.length === 0) {return true;}const request = context.switchToHttp().getRequest();const user = request.user;const ability: AppAbility = this.abilityFactory.createForUser(user);try {for (const req of requirements) {// req.subject 可能是字符串 'Article' 或 Article 实例const subject = req.subject;if (!ability.can(req.action as any, subject)) {// 使用 casl 的 ForbiddenError 可以提供更好信息throw new ForbiddenException('权限不足');}}return true;} catch (err) {if (err instanceof ForbiddenError || err instanceof ForbiddenException) {throw new ForbiddenException('权限不足');}throw err;}}
}
4) 在 Controller 中使用 CASL
import { Controller, Get, UseGuards, Param } from '@nestjs/common';
import { CheckAbilities } from './check-abilities.decorator';
import { PoliciesGuard } from './policies.guard';
import { JwtAuthGuard } from './jwt-auth.guard';
import { ArticlesService } from './articles.service';@UseGuards(JwtAuthGuard, PoliciesGuard)
@Controller('articles')
export class ArticlesController {constructor(private articlesService: ArticlesService) {}@CheckAbilities({ action: 'read', subject: 'Article' })@Get()findAll() {return this.articlesService.findAll();}@CheckAbilities({ action: 'update', subject: 'Article' })@Get(':id')async updateExample(@Param('id') id: string) {// 这里更实际的做法是:// 获取文章实例 -> ability.can('update', articleInstance) —— 对象级检查return {};}
}
对象级检查:通常需要在 Guard 或服务中先拿到具体实例(如 article),然后用 ability.can(action, article) 来判断,因为条件(如
{ authorId: user.id }
)需要具体对象的字段。
示例对象级检查(在 Controller Action 中):
@Get(':id')
async getOne(@Param('id') id: string, @Req() req) {const article = await this.articlesService.findOne(+id);const ability = this.abilityFactory.createForUser(req.user);if (!ability.can('read', article)) {throw new ForbiddenException();}return article;
}
也可以把“对象预取 + casl 检查”逻辑封装成拦截器或自定义 Guard。