NestJS权限控制 #

权限控制概述 #

权限控制是确定用户是否有权执行特定操作的过程。常见的权限控制模型包括:

  • RBAC(基于角色的访问控制)
  • ABAC(基于属性的访问控制)
  • ACL(访问控制列表)

RBAC实现 #

定义角色和权限 #

typescript
export enum Role {
  USER = 'user',
  ADMIN = 'admin',
  SUPER_ADMIN = 'super_admin',
}

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',
}

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),
};

角色装饰器 #

typescript
import { SetMetadata } from '@nestjs/common';
import { Role } from './role.enum';

export const ROLES_KEY = 'roles';
export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles);

权限装饰器 #

typescript
import { SetMetadata } from '@nestjs/common';
import { Permission } from './permission.enum';

export const PERMISSIONS_KEY = 'permissions';
export const RequirePermissions = (...permissions: Permission[]) =>
  SetMetadata(PERMISSIONS_KEY, permissions);

角色守卫 #

typescript
import {
  Injectable,
  CanActivate,
  ExecutionContext,
  ForbiddenException,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Role } from './role.enum';
import { ROLES_KEY } from './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('User not authenticated');
    }

    const hasRole = requiredRoles.some(role => user.role === role);

    if (!hasRole) {
      throw new ForbiddenException(
        `Required roles: ${requiredRoles.join(', ')}`,
      );
    }

    return true;
  }
}

权限守卫 #

typescript
import {
  Injectable,
  CanActivate,
  ExecutionContext,
  ForbiddenException,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Permission, ROLE_PERMISSIONS } from './permission.enum';
import { PERMISSIONS_KEY } from './permissions.decorator';
import { Role } from './role.enum';

@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('User not authenticated');
    }

    const userPermissions = this.getUserPermissions(user.role);
    const hasAllPermissions = requiredPermissions.every(permission =>
      userPermissions.includes(permission),
    );

    if (!hasAllPermissions) {
      throw new ForbiddenException(
        `Required permissions: ${requiredPermissions.join(', ')}`,
      );
    }

    return true;
  }

  private getUserPermissions(role: Role): Permission[] {
    return ROLE_PERMISSIONS[role] || [];
  }
}

使用守卫 #

typescript
import { Controller, Get, Post, UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
import { RolesGuard } from './roles.guard';
import { PermissionsGuard } from './permissions.guard';
import { Roles } from './roles.decorator';
import { RequirePermissions } from './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 'All users';
  }

  @Post()
  @Roles(Role.ADMIN, Role.SUPER_ADMIN)
  @RequirePermissions(Permission.USER_CREATE)
  create() {
    return 'User created';
  }

  @Delete(':id')
  @Roles(Role.SUPER_ADMIN)
  @RequirePermissions(Permission.USER_DELETE)
  remove() {
    return 'User deleted';
  }
}

动态权限 #

数据库存储权限 #

typescript
import { Entity, Column, PrimaryGeneratedColumn, ManyToMany } from 'typeorm';

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  name: string;

  @ManyToMany(() => Role, role => role.users)
  roles: Role[];
}

@Entity()
export class Role {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  name: string;

  @ManyToMany(() => Permission, permission => permission.roles)
  permissions: Permission[];
}

@Entity()
export class Permission {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  name: string;

  @Column()
  resource: string;

  @Column()
  action: string;
}

权限服务 #

typescript
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 [];

    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);
  }
}

动态权限守卫 #

typescript
import {
  Injectable,
  CanActivate,
  ExecutionContext,
  ForbiddenException,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { PermissionService } from './permission.service';
import { PERMISSIONS_KEY } from './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('User not authenticated');
    }

    const userPermissions = await this.permissionService.getUserPermissions(
      user.userId,
    );

    const hasAllPermissions = requiredPermissions.every(permission =>
      userPermissions.includes(permission),
    );

    if (!hasAllPermissions) {
      throw new ForbiddenException('Insufficient permissions');
    }

    return true;
  }
}

资源级权限 #

检查资源所有权 #

typescript
import {
  Injectable,
  CanActivate,
  ExecutionContext,
  ForbiddenException,
} from '@nestjs/common';

@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('Post not found');
    }

    if (post.authorId !== user.userId && user.role !== 'admin') {
      throw new ForbiddenException('You do not own this resource');
    }

    return true;
  }
}

使用资源所有权守卫 #

typescript
@Put(':id')
@UseGuards(JwtAuthGuard, ResourceOwnerGuard)
update(
  @Param('id') id: string,
  @Body() updatePostDto: UpdatePostDto,
) {
  return this.postsService.update(+id, updatePostDto);
}

ABAC实现 #

属性装饰器 #

typescript
import { SetMetadata } from '@nestjs/common';

export const POLICY_KEY = 'policy';
export const Policy = (policy: PolicyHandler) =>
  SetMetadata(POLICY_KEY, policy);

export type PolicyHandler = (
  user: any,
  resource: any,
) => boolean | Promise<boolean>;

ABAC守卫 #

typescript
import {
  Injectable,
  CanActivate,
  ExecutionContext,
  ForbiddenException,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { POLICY_KEY, PolicyHandler } from './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('User not authenticated');
    }

    const resource = { ...params, ...body };
    const canAccess = await policyHandler(user, resource);

    if (!canAccess) {
      throw new ForbiddenException('Access denied by policy');
    }

    return true;
  }
}

使用ABAC #

typescript
const canUpdatePost: PolicyHandler = (user, resource) => {
  return user.role === 'admin' || resource.authorId === user.userId;
};

@Put(':id')
@Policy(canUpdatePost)
update(@Param('id') id: string, @Body() updatePostDto: UpdatePostDto) {
  return this.postsService.update(+id, updatePostDto);
}

CASL集成 #

安装CASL #

bash
npm install @casl/ability

定义能力 #

typescript
import { Ability, AbilityBuilder, AbilityClass, ExtractSubjectType } from '@casl/ability';
import { User } from './entities/user.entity';
import { Post } from './entities/post.entity';

type Actions = 'manage' | 'create' | 'read' | 'update' | 'delete';
type Subjects = 'User' | 'Post' | User | Post | 'all';

export type AppAbility = Ability<[Actions, Subjects]>;

export function createAbilityForUser(user: any) {
  const { can, cannot, build } = new AbilityBuilder<AppAbility>(
    Ability as AbilityClass<AppAbility>,
  );

  if (user.role === 'admin') {
    can('manage', 'all');
  } else {
    can('read', 'Post');
    can('create', 'Post');
    can('update', 'Post', { authorId: user.id });
    can('delete', 'Post', { authorId: user.id });
  }

  return build();
}

CASL守卫 #

typescript
import {
  Injectable,
  CanActivate,
  ExecutionContext,
  ForbiddenException,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Ability, subject } from '@casl/ability';
import { CHECK_ABILITY, RequiredRule } from './ability.decorator';

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

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const rules =
      this.reflector.get<RequiredRule[]>(CHECK_ABILITY, context.getHandler()) ||
      [];

    const { user } = context.switchToHttp().getRequest();
    const ability = createAbilityForUser(user);

    const can = rules.every(rule =>
      ability.can(rule.action, subject(rule.subject, rule.resource)),
    );

    if (!can) {
      throw new ForbiddenException('Access denied');
    }

    return true;
  }
}

总结 #

本章学习了NestJS权限控制:

  • RBAC角色权限控制
  • 动态权限管理
  • 资源级权限
  • ABAC属性权限
  • CASL集成

接下来,让我们学习 自定义装饰器

最后更新:2026-03-28