Chuyển đến nội dung chính

第 10 課:警衛與授權 - RBAC

NestJS、AuthGuard、RolesGuard 中的守衛。基於角色的存取控制(RBAC),基於權限的授權。自訂裝飾器@Roles()、@Public()。 CASL 整合。

💻 程式設計 — 第 10 課 第 10 課:警衛與授權 - RBAC

NestJS:從基礎到高級

第 3 部分:身份驗證和安全性

亞洲開發網

1. 什麼是守衛?

警衛決定是否允許請求繼續。他們跑 在中間件之後,在攔截器和管道之前。主要用例: 授權 — 檢查使用者是否有存取資源的權限。

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

@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    const request = context.switchToHttp().getRequest();
    return !!request.user;  // true = cho phép, false = 403
  }
}

2.基於角色的存取控制(RBAC)

自訂 @Roles() 裝飾器

// auth/decorators/roles.decorator.ts
import { SetMetadata } from '@nestjs/common';

export enum Role {
  Admin = 'admin',
  Editor = 'editor',
  User = 'user',
}

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

RolesGuard實施

// auth/guards/roles.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ROLES_KEY, Role } from '../decorators/roles.decorator';

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

  canActivate(context: ExecutionContext): boolean {
    // Lấy roles yêu cầu từ metadata
    const requiredRoles = this.reflector.getAllAndOverride<Role[]>(ROLES_KEY, [
      context.getHandler(),  // Method-level
      context.getClass(),    // Class-level
    ]);

    // Nếu không có @Roles() → cho phép mọi user
    if (!requiredRoles) return true;

    const { user } = context.switchToHttp().getRequest();
    return requiredRoles.includes(user.role);
  }
}

使用

@Controller('users')
@UseGuards(JwtAuthGuard, RolesGuard)  // Apply cho toàn controller
export class UsersController {
  
  @Get()
  @Roles(Role.Admin, Role.Editor)  // Chỉ admin và editor
  findAll() { ... }

  @Get(':id')
  @Roles(Role.Admin)  // Chỉ admin
  findOne(@Param('id') id: string) { ... }

  @Post()
  @Roles(Role.Admin)
  create(@Body() dto: CreateUserDto) { ... }

  @Delete(':id')
  @Roles(Role.Admin)
  remove(@Param('id') id: string) { ... }
}

3. 基於權限的授權

// Permissions decorator
export enum Permission {
  CreateUser = 'user:create',
  ReadUser = 'user:read',
  UpdateUser = 'user:update',
  DeleteUser = 'user:delete',
  ManageOrders = 'order:manage',
}

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

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

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

    const { user } = context.switchToHttp().getRequest();
    return required.every(perm => user.permissions?.includes(perm));
  }
}

// Sử dụng
@RequirePermissions(Permission.DeleteUser)
@Delete(':id')
remove(@Param('id') id: string) { ... }

4. 資源所有者守護

// Chỉ cho phép chủ sở hữu hoặc admin
@Injectable()
export class OwnerGuard implements CanActivate {
  constructor(private usersService: UsersService) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const request = context.switchToHttp().getRequest();
    const user = request.user;
    const resourceId = request.params.id;

    // Admin luôn được quyền
    if (user.role === 'admin') return true;

    // Kiểm tra ownership
    return user.id === resourceId;
  }
}

@UseGuards(JwtAuthGuard, OwnerGuard)
@Patch(':id')
updateProfile(@Param('id') id: string, @Body() dto: UpdateProfileDto) {
  // Chỉ user sở hữu profile hoặc admin mới cập nhật được
}

5.CASL——高級授權

npm install @casl/ability
// casl/casl-ability.factory.ts
import { AbilityBuilder, createMongoAbility, MongoAbility } from '@casl/ability';

export type AppAbility = MongoAbility;

@Injectable()
export class CaslAbilityFactory {
  createForUser(user: User): AppAbility {
    const { can, cannot, build } = new AbilityBuilder(createMongoAbility);

    if (user.role === 'admin') {
      can('manage', 'all');  // Admin có mọi quyền
    } else {
      can('read', 'Post');
      can('create', 'Post');
      can('update', 'Post', { authorId: user.id });  // Chỉ update post của mình
      can('delete', 'Post', { authorId: user.id });
      can('read', 'User', { id: user.id });           // Chỉ xem profile mình
      can('update', 'User', { id: user.id });
      cannot('delete', 'User');                        // Không được xóa user
    }

    return build();
  }
}

// Abilities Guard
@Injectable()
export class AbilitiesGuard implements CanActivate {
  constructor(
    private reflector: Reflector,
    private caslAbilityFactory: CaslAbilityFactory,
  ) {}

  canActivate(context: ExecutionContext): boolean {
    const rules = this.reflector.get('check-abilities', context.getHandler());
    if (!rules) return true;

    const { user } = context.switchToHttp().getRequest();
    const ability = this.caslAbilityFactory.createForUser(user);

    return rules.every(rule => ability.can(rule.action, rule.subject));
  }
}

6. 複合Auth裝飾器

// Kết hợp nhiều guards thành 1 decorator gọn
import { applyDecorators, UseGuards } from '@nestjs/common';

export function Auth(...roles: Role[]) {
  return applyDecorators(
    Roles(...roles),
    UseGuards(JwtAuthGuard, RolesGuard),
  );
}

// Sử dụng cực gọn
@Auth(Role.Admin)
@Delete(':id')
remove(@Param('id') id: string) { ... }

@Auth(Role.Admin, Role.Editor)
@Post()
create(@Body() dto: CreatePostDto) { ... }

七、總結

  • 衛兵:決定請求是否繼續(canActivate)
  • RBAC:透過 @Roles() + RolesGuard 基於角色
  • 權限:細粒度的權限字串
  • 業主衛隊:資源所有權檢查
  • CASL:針對複雜邏輯的基於屬性的存取控制
  • 複合裝飾器: @Auth() 聚合多個守衛

下一篇文章將探討 安全最佳實踐 在 NestJS 中。