NestJSでセキュアなAPIを構築する際、ロールベースアクセス制御(RBAC: Role-Based Access Control)は欠かせない要素です。管理者のみがアクセスできるエンドポイント、一般ユーザーには読み取り専用など、ロールに応じたきめ細かな権限制御を実装することで、堅牢なアプリケーションを構築できます。本記事では、@Roles()カスタムデコレータの作成からRolesGuardの実装、Reflectorを使用したメタデータ取得、そして階層的なロール設計パターンまでを実践的に解説します。

実行環境と前提条件

本記事の内容を実践するにあたり、以下の環境を前提としています。

項目 バージョン・要件
Node.js 20以上
npm 10以上
NestJS 11.x
@nestjs/jwt 11.x
TypeScript 5.x
OS Windows / macOS / Linux
エディタ VS Code(推奨)

事前に以下の準備を完了してください。

  • NestJS CLIのインストール済み
  • NestJSプロジェクトの作成済み
  • JWT認証の実装済み(request.userにユーザー情報が格納されている状態)
  • Guardの基本概念の理解

NestJSプロジェクトの作成方法はNestJS入門記事、Guardの基本はGuard解説記事、JWT認証の実装はJWT認証記事を参照してください。

RBACとは何か

RBAC(Role-Based Access Control)は、ユーザーにロール(役割)を割り当て、そのロールに基づいてリソースへのアクセス権限を制御する手法です。

flowchart TD
    subgraph Users["ユーザー"]
        U1[田中さん]
        U2[鈴木さん]
        U3[佐藤さん]
    end
    
    subgraph Roles["ロール"]
        R1[Admin]
        R2[Moderator]
        R3[User]
    end
    
    subgraph Permissions["権限"]
        P1[全リソース管理]
        P2[コンテンツ編集]
        P3[閲覧のみ]
    end
    
    U1 --> R1
    U2 --> R2
    U3 --> R3
    
    R1 --> P1
    R2 --> P2
    R3 --> P3
    
    style R1 fill:#E91E63,color:#fff
    style R2 fill:#9C27B0,color:#fff
    style R3 fill:#3F51B5,color:#fff

RBACの利点

利点 説明
管理の簡素化 ユーザーごとに権限を設定する代わりにロールを割り当てるだけで済む
一貫性の確保 同じロールを持つユーザーは同じ権限を持つことが保証される
監査の容易さ ロールと権限の対応が明確なためセキュリティ監査がしやすい
スケーラビリティ ユーザー数が増えても管理コストが増大しにくい

NestJSにおけるRBACの設計アプローチ

NestJSでRBACを実装する際は、以下のコンポーネントを組み合わせます。

flowchart LR
    A[リクエスト] --> B[AuthGuard]
    B --> C{認証済み?}
    C -->|No| D[401 Unauthorized]
    C -->|Yes| E[RolesGuard]
    E --> F{ロール確認}
    F --> G["@Roles()メタデータ取得"]
    G --> H{必要なロールを持つ?}
    H -->|No| I[403 Forbidden]
    H -->|Yes| J[Controller]
    
    style E fill:#FF5722,color:#fff
    style G fill:#4CAF50,color:#fff
コンポーネント 役割
Role Enum システム内で使用可能なロールを定義
@Roles()デコレータ ルートハンドラに必要なロールをメタデータとして付与
RolesGuard メタデータを読み取りユーザーのロールと照合
Reflector デコレータで設定されたメタデータを取得

ロールの定義

まず、システムで使用するロールをEnumとして定義します。

Role Enumの作成

1
2
3
4
5
6
7
// src/auth/enums/role.enum.ts
export enum Role {
  User = 'user',
  Moderator = 'moderator',
  Admin = 'admin',
  SuperAdmin = 'super_admin',
}

このEnumにより、ロール名を文字列リテラルとして管理するよりも型安全性が向上し、タイポによるバグを防止できます。

ユーザーエンティティへのロール追加

ユーザーエンティティにロール情報を含める必要があります。以下は典型的な実装例です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// src/users/entities/user.entity.ts
import { Role } from '../../auth/enums/role.enum';

export class User {
  id: number;
  username: string;
  email: string;
  password: string;
  roles: Role[];
  createdAt: Date;
  updatedAt: Date;
}

TypeORMやPrismaを使用している場合は、それぞれのスキーマ定義に合わせてロールを永続化します。

@Roles()カスタムデコレータの作成

NestJSでは、SetMetadata()またはReflector.createDecorator()を使用してカスタムデコレータを作成できます。それぞれの方法を解説します。

方法1: SetMetadataを使用する(従来のアプローチ)

SetMetadata()は、任意のキーと値のペアをメタデータとしてルートハンドラに付与します。

1
2
3
4
5
6
// src/auth/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);

この方法では、ROLES_KEYを使用してメタデータを取得する必要があります。

方法2: Reflector.createDecoratorを使用する(推奨)

NestJS 10以降では、Reflector.createDecorator()を使用した型安全なアプローチが推奨されています。

1
2
3
4
5
// src/auth/decorators/roles.decorator.ts
import { Reflector } from '@nestjs/core';
import { Role } from '../enums/role.enum';

export const Roles = Reflector.createDecorator<Role[]>();

このアプローチの利点は以下のとおりです。

観点 SetMetadata Reflector.createDecorator
型安全性 手動で型を指定する必要あり 自動的に型推論される
キー管理 文字列キーを定義・管理する必要あり 不要(デコレータ自体がキーとなる)
コード量 やや冗長 シンプル

本記事では、Reflector.createDecorator()を使用したアプローチで進めます。

デコレータの使用例

作成した@Roles()デコレータは、Controllerのメソッドまたはクラスに適用できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// src/articles/articles.controller.ts
import { Controller, Get, Post, Body, Delete, Param } from '@nestjs/common';
import { Roles } from '../auth/decorators/roles.decorator';
import { Role } from '../auth/enums/role.enum';
import { ArticlesService } from './articles.service';
import { CreateArticleDto } from './dto/create-article.dto';

@Controller('articles')
export class ArticlesController {
  constructor(private readonly articlesService: ArticlesService) {}

  // すべてのユーザーがアクセス可能(認証のみ必要)
  @Get()
  findAll() {
    return this.articlesService.findAll();
  }

  // Moderator以上のロールが必要
  @Post()
  @Roles(Role.Moderator, Role.Admin, Role.SuperAdmin)
  create(@Body() createArticleDto: CreateArticleDto) {
    return this.articlesService.create(createArticleDto);
  }

  // Admin以上のロールが必要
  @Delete(':id')
  @Roles(Role.Admin, Role.SuperAdmin)
  remove(@Param('id') id: string) {
    return this.articlesService.remove(+id);
  }
}

RolesGuardの実装

RolesGuardは、リクエストを処理する前にユーザーのロールを検証するGuardです。Reflectorを使用してメタデータを取得し、ユーザーのロールと照合します。

基本的なRolesGuardの実装

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// src/auth/guards/roles.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Role } from '../enums/role.enum';
import { Roles } 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, [
      context.getHandler(),
      context.getClass(),
    ]);

    // ロールが設定されていない場合はアクセスを許可
    if (!requiredRoles) {
      return true;
    }

    // リクエストからユーザー情報を取得
    const { user } = context.switchToHttp().getRequest();

    // ユーザーが必要なロールのいずれかを持っているか確認
    return requiredRoles.some((role) => user.roles?.includes(role));
  }
}

Reflectorのメソッド解説

Reflectorクラスは、メタデータを取得するための複数のメソッドを提供しています。

メソッド 説明 使用例
get() 指定したターゲット(ハンドラまたはクラス)からメタデータを取得 単一レベルでのメタデータ取得
getAllAndOverride() 複数ターゲットを検索し、最初に見つかったメタデータを返す メソッドレベルがクラスレベルを上書き
getAllAndMerge() 複数ターゲットのメタデータを結合して返す クラスとメソッドのロールを合算
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// get() の使用例
const roles = this.reflector.get(Roles, context.getHandler());

// getAllAndOverride() の使用例(推奨)
const roles = this.reflector.getAllAndOverride(Roles, [
  context.getHandler(), // メソッドレベルを優先
  context.getClass(),   // クラスレベルをフォールバック
]);

// getAllAndMerge() の使用例
const roles = this.reflector.getAllAndMerge(Roles, [
  context.getHandler(),
  context.getClass(),
]);
// 結果: メソッドとクラスのロールが結合された配列

メタデータ取得の優先順位

getAllAndOverride()を使用する場合、以下の優先順位でメタデータが取得されます。

flowchart TD
    A["getAllAndOverride()呼び出し"] --> B{メソッドに@Roles()あり?}
    B -->|Yes| C[メソッドのロールを返す]
    B -->|No| D{クラスに@Roles()あり?}
    D -->|Yes| E[クラスのロールを返す]
    D -->|No| F[undefined を返す]
    
    style C fill:#4CAF50,color:#fff
    style E fill:#2196F3,color:#fff
    style F fill:#9E9E9E,color:#fff

これにより、クラスレベルでデフォルトのロールを設定し、特定のメソッドで上書きすることが可能になります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@Controller('users')
@Roles(Role.User) // デフォルト: Userロール
export class UsersController {
  @Get()
  // Userロールが適用される
  findAll() {}

  @Delete(':id')
  @Roles(Role.Admin) // 上書き: Adminロールのみ
  remove(@Param('id') id: string) {}
}

Guardの登録と適用

RolesGuardを適用するには、複数の方法があります。

方法1: メソッドレベルでの適用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import { Controller, Post, UseGuards } from '@nestjs/common';
import { AuthGuard } from '../auth/guards/auth.guard';
import { RolesGuard } from '../auth/guards/roles.guard';
import { Roles } from '../auth/decorators/roles.decorator';
import { Role } from '../auth/enums/role.enum';

@Controller('admin')
export class AdminController {
  @Post('settings')
  @UseGuards(AuthGuard, RolesGuard)
  @Roles(Role.Admin)
  updateSettings() {
    return { message: 'Settings updated' };
  }
}

方法2: コントローラレベルでの適用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
@Controller('admin')
@UseGuards(AuthGuard, RolesGuard)
@Roles(Role.Admin)
export class AdminController {
  @Get('dashboard')
  getDashboard() {
    return { message: 'Admin dashboard' };
  }

  @Post('settings')
  updateSettings() {
    return { message: 'Settings updated' };
  }
}

方法3: グローバル登録(推奨)

認可チェックをアプリケーション全体に適用する場合は、APP_GUARDトークンを使用してグローバルに登録します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// src/app.module.ts
import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';
import { AuthGuard } from './auth/guards/auth.guard';
import { RolesGuard } from './auth/guards/roles.guard';

@Module({
  providers: [
    {
      provide: APP_GUARD,
      useClass: AuthGuard,
    },
    {
      provide: APP_GUARD,
      useClass: RolesGuard,
    },
  ],
})
export class AppModule {}

グローバル登録の場合、Guardは定義順に実行されます。AuthGuardが先に登録されているため、認証 → 認可の順序で処理されます。

@Public()デコレータとの組み合わせ

グローバルGuardを使用する場合、認証不要のエンドポイント(ログインなど)には@Public()デコレータを付与してスキップします。

1
2
3
4
5
// src/auth/decorators/public.decorator.ts
import { SetMetadata } from '@nestjs/common';

export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// src/auth/guards/auth.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { IS_PUBLIC_KEY } from '../decorators/public.decorator';

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

  canActivate(context: ExecutionContext): boolean {
    const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);

    if (isPublic) {
      return true;
    }

    // JWT検証などの認証ロジック
    const request = context.switchToHttp().getRequest();
    return this.validateRequest(request);
  }

  private validateRequest(request: any): boolean {
    // 実際のJWT検証ロジック
    return !!request.user;
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// src/auth/auth.controller.ts
import { Controller, Post, Body } from '@nestjs/common';
import { Public } from './decorators/public.decorator';

@Controller('auth')
export class AuthController {
  @Public()
  @Post('login')
  login(@Body() loginDto: LoginDto) {
    // ログイン処理
  }

  @Public()
  @Post('register')
  register(@Body() registerDto: RegisterDto) {
    // 登録処理
  }
}

階層的なロール設計パターン

実務では、ロールに階層構造を持たせることで、管理を簡素化できます。例えば、AdminModeratorの権限をすべて持ち、ModeratorUserの権限をすべて持つという設計です。

ロール階層の定義

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// src/auth/enums/role.enum.ts
export enum Role {
  User = 'user',
  Moderator = 'moderator',
  Admin = 'admin',
  SuperAdmin = 'super_admin',
}

// ロールの階層レベルを定義
export const ROLE_HIERARCHY: Record<Role, number> = {
  [Role.User]: 1,
  [Role.Moderator]: 2,
  [Role.Admin]: 3,
  [Role.SuperAdmin]: 4,
};

階層対応のRolesGuard

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// src/auth/guards/roles.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Role, ROLE_HIERARCHY } from '../enums/role.enum';
import { Roles } 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, [
      context.getHandler(),
      context.getClass(),
    ]);

    if (!requiredRoles || requiredRoles.length === 0) {
      return true;
    }

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

    if (!user || !user.roles) {
      return false;
    }

    // ユーザーの最高ロールレベルを取得
    const userMaxLevel = this.getMaxRoleLevel(user.roles);

    // 必要なロールの最低レベルを取得
    const requiredMinLevel = this.getMinRoleLevel(requiredRoles);

    // ユーザーのロールレベルが必要レベル以上であれば許可
    return userMaxLevel >= requiredMinLevel;
  }

  private getMaxRoleLevel(roles: Role[]): number {
    return Math.max(...roles.map((role) => ROLE_HIERARCHY[role] || 0));
  }

  private getMinRoleLevel(roles: Role[]): number {
    return Math.min(...roles.map((role) => ROLE_HIERARCHY[role] || 0));
  }
}

この実装により、@Roles(Role.Moderator)と指定すれば、ModeratorAdminSuperAdminのいずれかを持つユーザーがアクセスできます。

flowchart TD
    subgraph "階層的アクセス制御"
        SA[SuperAdmin<br/>Level 4] --> A[Admin<br/>Level 3]
        A --> M[Moderator<br/>Level 2]
        M --> U[User<br/>Level 1]
    end
    
    subgraph "アクセス可能なリソース"
        R1["@Roles(User)<br/>Level 1+"]
        R2["@Roles(Moderator)<br/>Level 2+"]
        R3["@Roles(Admin)<br/>Level 3+"]
        R4["@Roles(SuperAdmin)<br/>Level 4"]
    end
    
    SA -.-> R1
    SA -.-> R2
    SA -.-> R3
    SA -.-> R4
    
    A -.-> R1
    A -.-> R2
    A -.-> R3
    
    M -.-> R1
    M -.-> R2
    
    U -.-> R1
    
    style SA fill:#E91E63,color:#fff
    style A fill:#9C27B0,color:#fff
    style M fill:#3F51B5,color:#fff
    style U fill:#00BCD4,color:#fff

使用例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Controller('articles')
@UseGuards(AuthGuard, RolesGuard)
export class ArticlesController {
  // すべての認証済みユーザーがアクセス可能
  @Get()
  @Roles(Role.User)
  findAll() {
    return this.articlesService.findAll();
  }

  // Moderator以上がアクセス可能
  @Post()
  @Roles(Role.Moderator)
  create(@Body() dto: CreateArticleDto) {
    return this.articlesService.create(dto);
  }

  // Admin以上がアクセス可能
  @Delete(':id')
  @Roles(Role.Admin)
  remove(@Param('id') id: string) {
    return this.articlesService.remove(+id);
  }

  // SuperAdminのみアクセス可能
  @Post('bulk-delete')
  @Roles(Role.SuperAdmin)
  bulkDelete(@Body() ids: number[]) {
    return this.articlesService.bulkDelete(ids);
  }
}

カスタムエラーレスポンスの実装

デフォルトでは、Guardがfalseを返すとForbiddenExceptionがスローされ、以下のレスポンスが返されます。

1
2
3
4
5
{
  "statusCode": 403,
  "message": "Forbidden resource",
  "error": "Forbidden"
}

より詳細なエラーメッセージを返したい場合は、カスタム例外をスローします。

カスタム例外の作成

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// src/common/exceptions/insufficient-role.exception.ts
import { ForbiddenException } from '@nestjs/common';
import { Role } from '../../auth/enums/role.enum';

export class InsufficientRoleException extends ForbiddenException {
  constructor(requiredRoles: Role[], userRoles: Role[]) {
    super({
      statusCode: 403,
      error: 'Forbidden',
      message: 'アクセス権限が不足しています',
      requiredRoles,
      userRoles,
    });
  }
}

RolesGuardでのカスタム例外使用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// src/auth/guards/roles.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Role, ROLE_HIERARCHY } from '../enums/role.enum';
import { Roles } from '../decorators/roles.decorator';
import { InsufficientRoleException } from '../../common/exceptions/insufficient-role.exception';

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

  canActivate(context: ExecutionContext): boolean {
    const requiredRoles = this.reflector.getAllAndOverride<Role[]>(Roles, [
      context.getHandler(),
      context.getClass(),
    ]);

    if (!requiredRoles || requiredRoles.length === 0) {
      return true;
    }

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

    if (!user || !user.roles) {
      throw new InsufficientRoleException(requiredRoles, []);
    }

    const userMaxLevel = this.getMaxRoleLevel(user.roles);
    const requiredMinLevel = this.getMinRoleLevel(requiredRoles);

    if (userMaxLevel < requiredMinLevel) {
      throw new InsufficientRoleException(requiredRoles, user.roles);
    }

    return true;
  }

  private getMaxRoleLevel(roles: Role[]): number {
    return Math.max(...roles.map((role) => ROLE_HIERARCHY[role] || 0));
  }

  private getMinRoleLevel(roles: Role[]): number {
    return Math.min(...roles.map((role) => ROLE_HIERARCHY[role] || 0));
  }
}

実装のテスト

RBACの動作を検証するためのテストコードを作成します。

RolesGuardのユニットテスト

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
// src/auth/guards/roles.guard.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { Reflector } from '@nestjs/core';
import { ExecutionContext } from '@nestjs/common';
import { RolesGuard } from './roles.guard';
import { Role } from '../enums/role.enum';

describe('RolesGuard', () => {
  let guard: RolesGuard;
  let reflector: Reflector;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [RolesGuard, Reflector],
    }).compile();

    guard = module.get<RolesGuard>(RolesGuard);
    reflector = module.get<Reflector>(Reflector);
  });

  const mockExecutionContext = (user: any): ExecutionContext => {
    return {
      switchToHttp: () => ({
        getRequest: () => ({ user }),
      }),
      getHandler: () => jest.fn(),
      getClass: () => jest.fn(),
    } as unknown as ExecutionContext;
  };

  it('ロールが設定されていない場合はtrueを返す', () => {
    jest.spyOn(reflector, 'getAllAndOverride').mockReturnValue(null);
    const context = mockExecutionContext({ roles: [Role.User] });

    expect(guard.canActivate(context)).toBe(true);
  });

  it('ユーザーが必要なロールを持っている場合はtrueを返す', () => {
    jest.spyOn(reflector, 'getAllAndOverride').mockReturnValue([Role.Admin]);
    const context = mockExecutionContext({ roles: [Role.Admin] });

    expect(guard.canActivate(context)).toBe(true);
  });

  it('上位ロールは下位ロールの権限にアクセスできる', () => {
    jest.spyOn(reflector, 'getAllAndOverride').mockReturnValue([Role.Moderator]);
    const context = mockExecutionContext({ roles: [Role.Admin] });

    expect(guard.canActivate(context)).toBe(true);
  });

  it('ユーザーが必要なロールを持っていない場合は例外をスロー', () => {
    jest.spyOn(reflector, 'getAllAndOverride').mockReturnValue([Role.Admin]);
    const context = mockExecutionContext({ roles: [Role.User] });

    expect(() => guard.canActivate(context)).toThrow();
  });

  it('ユーザー情報がない場合は例外をスロー', () => {
    jest.spyOn(reflector, 'getAllAndOverride').mockReturnValue([Role.User]);
    const context = mockExecutionContext(null);

    expect(() => guard.canActivate(context)).toThrow();
  });
});

テストの実行

1
npm run test -- --testPathPattern=roles.guard

期待される出力は以下のとおりです。

1
2
3
4
5
6
7
PASS  src/auth/guards/roles.guard.spec.ts
  RolesGuard
    ✓ ロールが設定されていない場合はtrueを返す (3 ms)
    ✓ ユーザーが必要なロールを持っている場合はtrueを返す (1 ms)
    ✓ 上位ロールは下位ロールの権限にアクセスできる (1 ms)
    ✓ ユーザーが必要なロールを持っていない場合は例外をスロー (2 ms)
    ✓ ユーザー情報がない場合は例外をスロー (1 ms)

完成したコードの全体構成

最終的なディレクトリ構成は以下のようになります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
src/
├── app.module.ts
├── auth/
│   ├── auth.module.ts
│   ├── decorators/
│   │   ├── public.decorator.ts
│   │   └── roles.decorator.ts
│   ├── enums/
│   │   └── role.enum.ts
│   └── guards/
│       ├── auth.guard.ts
│       ├── roles.guard.ts
│       └── roles.guard.spec.ts
├── common/
│   └── exceptions/
│       └── insufficient-role.exception.ts
└── articles/
    ├── articles.module.ts
    ├── articles.controller.ts
    └── articles.service.ts

まとめ

本記事では、NestJSにおけるRBAC(ロールベースアクセス制御)の実装方法を解説しました。

項目 内容
Role Enum システムで使用するロールを型安全に定義
@Roles()デコレータ Reflector.createDecorator()で型安全なカスタムデコレータを作成
RolesGuard Reflectorでメタデータを取得しユーザーロールと照合
階層的ロール ロールレベルを数値で管理し上位ロールが下位権限にアクセス可能
グローバル登録 APP_GUARDで認証・認可Guardを一括適用

RBACを正しく実装することで、セキュアで保守性の高いAPIを構築できます。さらに高度な認可制御が必要な場合は、CASLライブラリを使用した属性ベースアクセス制御(ABAC)の導入も検討してください。

参考リンク