权限控制
本教程共 47 篇 · 第 22 篇 · 更新于 2026-08-09 · 约 12 分钟阅读
本节目标:学会在 NestJS 中实现权限控制,包括 RBAC 角色模型、细粒度权限检查和 CASL 权限库集成,让你的 API 真正做到”谁该干啥就干啥”。
上一章我们用 JWT 解决了”你是谁”的问题。这一章要解决的是”你能干啥”。
打个比方:门禁卡让你进了大楼(认证),但不同楼层的门禁权限不一样——你能进办公区,不一定能进机房。这就是权限控制(Authorization)干的事。
权限控制的几种模型
在动手写代码之前,先了解常见的权限模型,心里有个数:
| 模型 | 全称 | 核心思路 | 适合场景 |
|---|---|---|---|
| RBAC | 基于角色的访问控制 | 用户 -> 角色 -> 权限 | 大多数后台管理系统 |
| ABAC | 基于属性的访问控制 | 根据用户/资源属性动态判断 | 复杂业务规则 |
| ACL | 访问控制列表 | 逐个列出谁对什么有什么权限 | 简单的小项目 |
实际项目中,RBAC 用得最多。我们就从它开始。
RBAC:基于角色的权限控制
第一步:定义角色和权限枚举
先把角色和权限用枚举定义清楚。这就像给公司定组织架构——先有哪些角色,每个角色能干什么。
// enums/role.enum.ts
export enum Role {
USER = 'user',
ADMIN = 'admin',
SUPER_ADMIN = 'super_admin',
}
// enums/permission.enum.ts
export enum Permission {
USER_READ = 'user:read',
USER_CREATE = 'user:create',
USER_UPDATE = 'user:update',
USER_DELETE = 'user:delete',
POST_READ = 'post:read',
POST_CREATE = 'post:create',
POST_UPDATE = 'post:update',
POST_DELETE = 'post:delete',
}
Tip权限用
资源:操作的格式命名,比如user:create、post:delete。这种命名方式一目了然,后期维护也方便。
第二步:建立角色-权限映射
接下来定义每个角色拥有哪些权限。你可以把它理解成一张”权限表”。
// config/role-permissions.ts
import { Role } from '../enums/role.enum';
import { Permission } from '../enums/permission.enum';
export const ROLE_PERMISSIONS: Record<Role, Permission[]> = {
[Role.USER]: [
Permission.USER_READ,
Permission.POST_READ,
Permission.POST_CREATE,
],
[Role.ADMIN]: [
Permission.USER_READ,
Permission.USER_CREATE,
Permission.USER_UPDATE,
Permission.POST_READ,
Permission.POST_CREATE,
Permission.POST_UPDATE,
Permission.POST_DELETE,
],
[Role.SUPER_ADMIN]: Object.values(Permission), // 超级管理员拥有所有权限
};
第三步:创建装饰器
回忆一下第 19 章学的自定义装饰器。我们需要两个装饰器:一个标记路由需要的角色,一个标记需要的权限。
// decorators/roles.decorator.ts
import { SetMetadata } from '@nestjs/common';
import { Role } from '../enums/role.enum';
export const ROLES_KEY = 'roles';
export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles);
// decorators/permissions.decorator.ts
import { SetMetadata } from '@nestjs/common';
import { Permission } from '../enums/permission.enum';
export const PERMISSIONS_KEY = 'permissions';
export const RequirePermissions = (...permissions: Permission[]) =>
SetMetadata(PERMISSIONS_KEY, permissions);
用法和第 19 章一模一样——@SetMetadata 把信息”贴”到路由处理函数上,守卫再来”撕下来”读取。
第四步:编写角色守卫
角色守卫的逻辑很简单:取出路由需要的角色,看看当前用户有没有其中之一。
// guards/roles.guard.ts
import {
Injectable,
CanActivate,
ExecutionContext,
ForbiddenException,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Role } from '../enums/role.enum';
import { ROLES_KEY } from '../decorators/roles.decorator';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
// 读取路由上标记的角色要求
const requiredRoles = this.reflector.getAllAndOverride<Role[]>(
ROLES_KEY,
[context.getHandler(), context.getClass()],
);
// 没标记角色?说明不需要权限控制,直接放行
if (!requiredRoles) {
return true;
}
const { user } = context.switchToHttp().getRequest();
if (!user) {
throw new ForbiddenException('用户未认证');
}
// 用户拥有的角色中,只要有一个匹配就行
const hasRole = requiredRoles.some((role) => user.roles?.includes(role));
if (!hasRole) {
throw new ForbiddenException('角色权限不足');
}
return true;
}
}
Note
reflector.getAllAndOverride会先查方法上的元数据,再查类上的。如果方法上标了@Roles(Role.Admin),类上标了@Roles(Role.User),最终取到的是[Role.Admin]。如果你想取两者的合集,可以用reflector.getAllAndMerge。
第五步:编写权限守卫
权限守卫比角色守卫更进一步——它检查的不是”你是不是某个角色”,而是”你有没有某个具体权限”。
// guards/permissions.guard.ts
import {
Injectable,
CanActivate,
ExecutionContext,
ForbiddenException,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Permission } from '../enums/permission.enum';
import { ROLE_PERMISSIONS } from '../config/role-permissions';
import { Role } from '../enums/role.enum';
import { PERMISSIONS_KEY } from '../decorators/permissions.decorator';
@Injectable()
export class PermissionsGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredPermissions = this.reflector.getAllAndOverride<Permission[]>(
PERMISSIONS_KEY,
[context.getHandler(), context.getClass()],
);
if (!requiredPermissions) {
return true;
}
const { user } = context.switchToHttp().getRequest();
if (!user) {
throw new ForbiddenException('用户未认证');
}
// 根据用户角色,查出他拥有的所有权限
const userPermissions = this.getUserPermissions(user.role);
// 必须拥有全部所需权限才能通过
const hasAllPermissions = requiredPermissions.every((permission) =>
userPermissions.includes(permission),
);
if (!hasAllPermissions) {
throw new ForbiddenException('权限不足');
}
return true;
}
private getUserPermissions(role: Role): Permission[] {
return ROLE_PERMISSIONS[role] || [];
}
}
在控制器中使用
现在把守卫和装饰器配合起来用。假设我们有一个用户管理模块:
// users/users.controller.ts
import { Controller, Get, Post, Delete, UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
import { RolesGuard } from './guards/roles.guard';
import { PermissionsGuard } from './guards/permissions.guard';
import { Roles } from './decorators/roles.decorator';
import { RequirePermissions } from './decorators/permissions.decorator';
import { Role, Permission } from './enums';
@Controller('users')
@UseGuards(JwtAuthGuard, RolesGuard, PermissionsGuard)
export class UsersController {
@Get()
@Roles(Role.ADMIN, Role.SUPER_ADMIN)
@RequirePermissions(Permission.USER_READ)
findAll() {
return '获取用户列表';
}
@Post()
@Roles(Role.ADMIN, Role.SUPER_ADMIN)
@RequirePermissions(Permission.USER_CREATE)
create() {
return '创建用户';
}
@Delete(':id')
@Roles(Role.SUPER_ADMIN)
@RequirePermissions(Permission.USER_DELETE)
remove() {
return '删除用户';
}
}
请求到达时,守卫按顺序执行:
JwtAuthGuard—— 验证 JWT,确认用户身份RolesGuard—— 检查用户角色是否满足要求PermissionsGuard—— 检查用户是否拥有具体权限
三层都通过了,才执行控制器方法。
动态权限:从数据库读取
前面我们把角色-权限映射写死在代码里。小项目够用了,但实际项目中权限经常需要动态管理——管理员在后台界面上勾选分配权限,数据存在数据库里。
数据库实体设计
典型的 RBAC 数据库模型是这样的:用户和角色是多对多,角色和权限也是多对多。
// entities/user.entity.ts
import { Entity, Column, PrimaryGeneratedColumn, ManyToMany, JoinTable } from 'typeorm';
import { Role } from './role.entity';
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@ManyToMany(() => Role, (role) => role.users)
@JoinTable()
roles: Role[];
}
// entities/role.entity.ts
import { Entity, Column, PrimaryGeneratedColumn, ManyToMany } from 'typeorm';
import { Permission } from './permission.entity';
import { User } from './user.entity';
@Entity()
export class Role {
@PrimaryGeneratedColumn()
id: number;
@Column({ unique: true })
name: string;
@ManyToMany(() => Permission, (permission) => permission.roles)
@JoinTable()
permissions: Permission[];
@ManyToMany(() => User, (user) => user.roles)
users: User[];
}
// entities/permission.entity.ts
import { Entity, Column, PrimaryGeneratedColumn, ManyToMany } from 'typeorm';
import { Role } from './role.entity';
@Entity()
export class Permission {
@PrimaryGeneratedColumn()
id: number;
@Column({ unique: true })
name: string; // 例如 'user:create'
@Column()
resource: string; // 资源名,例如 'user'
@Column()
action: string; // 操作,例如 'create'
@ManyToMany(() => Role, (role) => role.permissions)
roles: Role[];
}
权限服务
写一个服务来查询用户的权限列表:
// services/permission.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './entities/user.entity';
@Injectable()
export class PermissionService {
constructor(
@InjectRepository(User)
private userRepository: Repository<User>,
) {}
async getUserPermissions(userId: number): Promise<string[]> {
const user = await this.userRepository.findOne({
where: { id: userId },
relations: ['roles', 'roles.permissions'],
});
if (!user) return [];
// 用 Set 去重,因为一个用户可能有多个角色,角色之间可能有重复权限
const permissions = new Set<string>();
user.roles.forEach((role) => {
role.permissions.forEach((permission) => {
permissions.add(permission.name);
});
});
return Array.from(permissions);
}
async hasPermission(userId: number, permission: string): Promise<boolean> {
const permissions = await this.getUserPermissions(userId);
return permissions.includes(permission);
}
}
动态权限守卫
把前面的静态权限守卫改成从数据库读取:
// guards/dynamic-permissions.guard.ts
import {
Injectable,
CanActivate,
ExecutionContext,
ForbiddenException,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { PermissionService } from '../services/permission.service';
import { PERMISSIONS_KEY } from '../decorators/permissions.decorator';
@Injectable()
export class DynamicPermissionsGuard implements CanActivate {
constructor(
private reflector: Reflector,
private permissionService: PermissionService,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const requiredPermissions = this.reflector.getAllAndOverride<string[]>(
PERMISSIONS_KEY,
[context.getHandler(), context.getClass()],
);
if (!requiredPermissions) {
return true;
}
const { user } = context.switchToHttp().getRequest();
if (!user) {
throw new ForbiddenException('用户未认证');
}
// 从数据库查询用户权限
const userPermissions = await this.permissionService.getUserPermissions(
user.userId,
);
const hasAllPermissions = requiredPermissions.every((permission) =>
userPermissions.includes(permission),
);
if (!hasAllPermissions) {
throw new ForbiddenException('权限不足');
}
return true;
}
}
Tip动态权限每次请求都查数据库,性能开销不小。生产环境中建议把用户权限缓存到 Redis,用户登录时写入缓存,权限变更时清除缓存。
资源级权限
RBAC 解决的是”能不能做某类操作”。但有时候我们需要更细粒度的控制——“能不能操作这个具体的资源”。
举个例子:用户可以编辑自己的文章,但不能编辑别人的。这就是资源级权限。
// guards/resource-owner.guard.ts
import {
Injectable,
CanActivate,
ExecutionContext,
ForbiddenException,
NotFoundException,
} from '@nestjs/common';
import { PostsService } from '../posts/posts.service';
@Injectable()
export class ResourceOwnerGuard implements CanActivate {
constructor(private postsService: PostsService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const { user, params } = request;
const postId = parseInt(params.id);
const post = await this.postsService.findOne(postId);
if (!post) {
throw new NotFoundException('文章不存在');
}
// 管理员可以操作任何文章,普通用户只能操作自己的
if (post.authorId !== user.userId && user.role !== 'admin') {
throw new ForbiddenException('你不是这篇文章的作者');
}
return true;
}
}
在控制器中使用:
@Put(':id')
@UseGuards(JwtAuthGuard, ResourceOwnerGuard)
update(
@Param('id') id: string,
@Body() updatePostDto: UpdatePostDto,
) {
return this.postsService.update(+id, updatePostDto);
}
Note资源级权限检查通常需要在守卫中查询数据库,所以
canActivate要声明为async,返回Promise<boolean>。
ABAC:基于属性的访问控制
有些业务规则用角色描述不了。比如:
- 用户只能编辑自己创建的文章
- 已发布的文章不能被删除
- 部门经理可以审批本部门的报销单
这些规则跟角色关系不大,主要看”用户属性”和”资源属性”之间的关系。这就是 ABAC 的思路。
定义策略类型
// types/policy.types.ts
export type PolicyHandler = (
user: any,
resource: any,
) => boolean | Promise<boolean>;
创建策略装饰器
// decorators/policy.decorator.ts
import { SetMetadata } from '@nestjs/common';
import { PolicyHandler } from '../types/policy.types';
export const POLICY_KEY = 'policy';
export const Policy = (handler: PolicyHandler) =>
SetMetadata(POLICY_KEY, handler);
创建策略守卫
// guards/policy.guard.ts
import {
Injectable,
CanActivate,
ExecutionContext,
ForbiddenException,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { PolicyHandler } from '../types/policy.types';
import { POLICY_KEY } from '../decorators/policy.decorator';
@Injectable()
export class PolicyGuard implements CanActivate {
constructor(private reflector: Reflector) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const policyHandler = this.reflector.getAllAndOverride<PolicyHandler>(
POLICY_KEY,
[context.getHandler(), context.getClass()],
);
if (!policyHandler) {
return true;
}
const request = context.switchToHttp().getRequest();
const { user, params, body } = request;
if (!user) {
throw new ForbiddenException('用户未认证');
}
const resource = { ...params, ...body };
const canAccess = await policyHandler(user, resource);
if (!canAccess) {
throw new ForbiddenException('不符合访问策略');
}
return true;
}
}
使用 ABAC
定义一个策略函数,然后贴到路由上:
// 策略:只有管理员或文章作者才能编辑
const canUpdatePost: PolicyHandler = (user, resource) => {
return user.role === 'admin' || Number(resource.authorId) === user.userId;
};
@Put(':id')
@UseGuards(JwtAuthGuard, PolicyGuard)
@Policy(canUpdatePost)
update(@Param('id') id: string, @Body() updatePostDto: UpdatePostDto) {
return this.postsService.update(+id, updatePostDto);
}
ABAC 的好处是灵活——策略函数想写多复杂就写多复杂。坏处是容易散落各处,不好统一管理。
CASL:更优雅的权限方案
当权限规则越来越多,散落各处的策略函数会变成噩梦。CASL 就是来解决这个问题的——它把权限规则集中定义,提供统一的查询接口。
安装
npm install @casl/ability
定义能力工厂
CASL 的核心概念是”能力”(Ability)。一个能力描述了”谁对什么能做什么”。
// casl/casl-ability.factory.ts
import { Injectable } from '@nestjs/common';
import {
MongoAbility,
AbilityBuilder,
createMongoAbility,
ExtractSubjectType,
InferSubjects,
} from '@casl/ability';
// 定义操作类型
export enum Action {
Manage = 'manage', // 所有操作
Create = 'create',
Read = 'read',
Update = 'update',
Delete = 'delete',
}
// 定义实体类(简化版)
export class Article {
id: number;
isPublished: boolean;
authorId: number;
}
type Subjects = InferSubjects<typeof Article> | 'all';
export type AppAbility = MongoAbility<[Action, Subjects]>;
@Injectable()
export class CaslAbilityFactory {
createForUser(user: any): AppAbility {
const { can, cannot, build } = new AbilityBuilder(createMongoAbility);
if (user.isAdmin) {
// 管理员可以做任何事
can(Action.Manage, 'all');
} else {
// 普通用户可以读所有文章
can(Action.Read, Article);
// 只能创建文章
can(Action.Create, Article);
// 只能更新自己的文章
can(Action.Update, Article, { authorId: user.id });
// 不能删除已发布的文章
cannot(Action.Delete, Article, { isPublished: true });
}
return build({
detectSubjectType: (item) =>
item.constructor as ExtractSubjectType<Subjects>,
});
}
}
这段代码定义了一套完整的权限规则。来看看怎么用:
const user = { id: 1, isAdmin: false };
const ability = caslAbilityFactory.createForUser(user);
const article = new Article();
article.authorId = 1;
ability.can(Action.Read, Article); // true —— 普通用户可以读
ability.can(Action.Create, Article); // true —— 普通用户可以创建
ability.can(Action.Update, article); // true —— 作者可以更新自己的文章
ability.can(Action.Delete, article); // true —— 未发布可以删
article.isPublished = true;
ability.can(Action.Delete, article); // false —— 已发布不能删
创建策略守卫
把 CASL 和 NestJS 守卫结合起来:
// casl/policies.guard.ts
import {
Injectable,
CanActivate,
ExecutionContext,
ForbiddenException,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { AppAbility, CaslAbilityFactory } from './casl-ability.factory';
// 策略处理器可以是函数或对象
interface IPolicyHandler {
handle(ability: AppAbility): boolean;
}
type PolicyHandlerCallback = (ability: AppAbility) => boolean;
export type PolicyHandler = IPolicyHandler | PolicyHandlerCallback;
// 装饰器
export const CHECK_POLICIES_KEY = 'check_policy';
export const CheckPolicies = (...handlers: PolicyHandler[]) =>
SetMetadata(CHECK_POLICIES_KEY, handlers);
@Injectable()
export class PoliciesGuard implements CanActivate {
constructor(
private reflector: Reflector,
private caslAbilityFactory: CaslAbilityFactory,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const policyHandlers =
this.reflector.get<PolicyHandler[]>(
CHECK_POLICIES_KEY,
context.getHandler(),
) || [];
const { user } = context.switchToHttp().getRequest();
const ability = this.caslAbilityFactory.createForUser(user);
// 所有策略都必须通过
return policyHandlers.every((handler) =>
this.execPolicyHandler(handler, ability),
);
}
private execPolicyHandler(handler: PolicyHandler, ability: AppAbility) {
if (typeof handler === 'function') {
return handler(ability);
}
return handler.handle(ability);
}
}
在控制器中使用 CASL
@Get()
@UseGuards(JwtAuthGuard, PoliciesGuard)
@CheckPolicies((ability: AppAbility) => ability.can(Action.Read, Article))
findAll() {
return this.articlesService.findAll();
}
@Delete(':id')
@UseGuards(JwtAuthGuard, PoliciesGuard)
@CheckPolicies((ability: AppAbility) => ability.can(Action.Delete, Article))
remove(@Param('id') id: string) {
return this.articlesService.remove(+id);
}
也可以用类的方式来定义策略,适合复杂逻辑:
export class DeleteArticlePolicyHandler implements IPolicyHandler {
handle(ability: AppAbility) {
return ability.can(Action.Delete, Article);
}
}
@Delete(':id')
@UseGuards(JwtAuthGuard, PoliciesGuard)
@CheckPolicies(new DeleteArticlePolicyHandler())
remove(@Param('id') id: string) {
return this.articlesService.remove(+id);
}
选哪种方案?
| 方案 | 优点 | 缺点 | 适合场景 |
|---|---|---|---|
| RBAC | 简单直观,容易理解 | 粒度粗,不够灵活 | 后台管理系统 |
| ABAC | 非常灵活 | 规则散落,难维护 | 规则少且变化多 |
| CASL | 集中管理,表达力强 | 有学习成本 | 中大型项目 |
小项目用 RBAC 就够了。当权限规则开始变得复杂,比如需要判断”用户只能操作自己的资源”,就该考虑 CASL 了。
Tip实际项目中经常混合使用:RBAC 做粗粒度的角色控制,CASL 或自定义守卫做细粒度的资源控制。不用纠结”只能选一种”。
踩坑经验
1. 守卫的执行顺序很重要
多个守卫按声明顺序执行。认证守卫一定要放在权限守卫前面——你总得先知道”这是谁”,才能判断”他能干啥”。
// 正确:先认证,再鉴权
@UseGuards(JwtAuthGuard, RolesGuard, PermissionsGuard)
// 错误:权限守卫在认证守卫前面,此时 request.user 还是空的
@UseGuards(RolesGuard, JwtAuthGuard)
2. 别忘了全局守卫
如果你的大部分接口都需要权限控制,考虑把权限守卫注册为全局守卫,然后对个别不需要权限的接口用 @Public() 装饰器跳过。
3. 缓存用户权限
动态权限每次查数据库会很慢。用户登录后把权限列表存到 JWT 的 payload 里,或者存到 Redis,能大幅减少数据库查询。
4. 权限变更要及时生效
如果用户权限改了(比如从普通用户升级为管理员),旧的 JWT 里还存着旧权限。解决方案:缩短 JWT 过期时间 + 配合 Refresh Token,或者在 Redis 中维护一个”权限版本号”。
小结
关键知识点回顾:
- RBAC 用角色绑定权限,用户分配角色,适合大多数后台系统
- 用
SetMetadata+Reflector实现细粒度的权限声明 - CASL 提供基于属性的访问控制,适合复杂业务场景
- 守卫的执行顺序很重要:先认证,再鉴权
- 生产环境注意缓存用户权限、及时处理权限变更
下一章我们聊聊安全加固,把 API 的安全防线建起来。