🛡

【公式ドキュメント和訳】NestJS Guards

commits9 min read

この記事は、2021/11/7時点のNestJSの仕様に依存します。
NestJSのバージョンが違ったり、記事の公開から長い時間が経っている場合、正確ではない可能性があります。

NestJS: v8.0.0
翻訳元: guards.md
ライセンス: MIT License

Guards

Guardは@Injectable()デコレータのついたクラスのことです。GuardはCanActivateインターフェースを実装している必要があります。

guard-1

Guardには、単一責任性があります。ランタイム時に権限やロール、ACLなどの状態から、与えられたリクエストがルートハンドラーによって処理されるべきかどうかを判断します。これはよく認可と呼ばれます。認可は認証と似ていて一緒に採用されることも多いですが、Expressアプリケーションにおいてはミドルウェアで処理されます。ミドルウェアは認証に向いています。なぜなら、トークンの検証や、requestオブジェクトにプロパティを付与するタスクは、特定のルートやそのメタデータに大きく依存することではないからです。

しかし、ミドルウェアはそのままではかなり貧弱な機能です。なぜかというと、ミドルウェア自身は、next()関数を呼び出した後にどのような処理が実行されるかを知らないからです。一方、GuardではExecutionContextインスタンスにアクセスできるので、次に何が実行されるのかを知ることができます。ExceptionフィルタやPipe、Interceptorといった機能と同様に、リクエストとレスポンスのサイクルにロジックを宣言的に挿入できるようにデザインされています。これらの機能を使うことによって、コードをDRYで宣言的な状態に保つことができます。

Guardはミドルウェアの、interceptorやpipeのに実行されます。

Guardを使用した認可

前述のように、認可においてはユーザが権限を持っている場合のみルートを使用できる必要があるため、Guardを利用すると便利です。ここでは、リクエストヘッダにトークンが付与されているユーザを認証済みとみなすためのAuthGuardを実装してみましょう。トークンを取り出して検証し、そのトークンの情報を元にリクエストが引き続き処理されるべきかを判断します。

auth.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';

@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    const request = context.switchToHttp().getRequest();
    return validateRequest(request);
  }
}

認証を実装した実際の例をお探しなら、この章をご覧ください。認可のサンプルをもっとみたい場合は、このページを参照してください。

validateRequest()関数内のロジックは、シンプルなものです。このサンプルで理解していただきたいのは、Guardがどのようにしてリクエストとレスポンスのサイクルに組み込まれているかという点です。

Guardは、canActivate()関数を実装する必要があります。この関数は、リクエストが許可されるかどうかを示すbooleanを返します。返す値は、同期でも、PromiseObservableを使用した非同期でも問題ありません。Nestは返された値によって以下のような動作をします。

  • trueが返されると、リクエストは引き続き処理されます。
  • falseが返されると、リクエストは拒否されます。

実行のコンテキスト

canActivate()関数は、ExecutionContextインスタンスを引数に取ります。ExecutionContextは、ArgumentsHostから引き継がれます。ArgumentsHostはExceptionフィルタの章でも登場しました。下のサンプルでは、Requestオブジェクトを参照するために、ArgumentsHostで定義したヘルパーメソッドを使用します。詳しくは、Exceptionフィルタの章のArguments hostを参照してください。

ExecutionContextArgumentsHostを継承しており、現在の実行プロセスの詳細を取得する新しいヘルパーメソッドが追加されています。実行プロセスの詳細な情報を取得できるので、幅広いコントローラやメソッド、実行コンテキストで汎用的に動作するGuardを作成することができます。詳しくは、ここExecutionContextを参照してください。

ロールを使用した認証

特定のロールを持ったユーザのみにアクセス権限を与えるGuardを作成しましょう。基本的なGuardのテンプレートを使いましょう。実装は次の章で行います。下のテンプレートの状態だと、全てのリクエストを許可しています。

roles.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';

@Injectable()
export class RolesGuard implements CanActivate {
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    return true;
  }
}

Guardをバインドする

PipeやExceptionフィルタと同じく、Guardもコントローラのスコープ、メソッドのスコープ、グローバルのスコープを指定できます。以下では、@UseGuards()デコレータを使用して、コントローラのスコープのGuardを実装してみます。このデコレータは1つ引数、もしくはコンマ区切りのリストを引数に取ります。なので、一つのデコレーションで簡単にGuardを適用できます。

@Controller('cats')
@UseGuards(RolesGuard)
export class CatsController {}

@UseGuards()デコレータは@nestjs/commonパッケージからインポートしてください。

上のコードでは、インスタンスの代わりにRolesGuardタイプを渡し、インスタンス化をフレームワークに任せることでDI(dependency injection)を有効化しています。PipeやExceptionフィルタと同じく、インスタンスを渡すこともできます。

@Controller('cats')
@UseGuards(new RolesGuard())
export class CatsController {}

上の書き方だと、このコントローラ内で定義される全てのハンドラにGuardが適用されます。Guardを一つのメソッドにのみ適用したい場合は、@UseGuards()デコレータをメソッドに対してで適用してください。グローバルに対してGuardを適用したい場合は、NestのアプリケーションインスタンスにあるメソッドのuseGlobalGuards()を使用してください。

const app = await NestFactory.create(AppModule);
app.useGlobalGuards(new RolesGuard());

ハイブリッドアプリの場合、デフォルトで、useGlobalGuards()メソッドはゲートウェイとマイクロサービスに対してGuardを適用しません。(変更する方法は、ここに書いてあります。)ハイブリッドではないスタンダードなマイクロサービスアプリケーションの場合、useGlobalGuards()はグローバルに機能します。

グローバルなGuardはアプリケーション全体の全てのコントローラとルートハンドラに適用されます。DIに関しては、グローバルなGuardはuseGlobalGuards()によってモジュールの外側で登録されているので、DIを使用することができません。解決策としては、以下のような構文でモジュールに直接Guardを設定する方法があります。

app.module.ts
import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';

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

この方法を使ってGuardでDIを使用するときに気をつけなければいけないのは、これがどこのモジュールに書かれているかに関係なく、Guardがグローバルであるという点です。では、どこに書くべきなのでしょうか。答えは、Guardが定義されているモジュールです。上のサンプルの場合だと、RolesGuardが定義されているモジュールということになります。
また、カスタムプロバイダの登録はuseClass以外でも可能です。詳しくは、ここをご参照ください。

ハンドラ単位でロールを設定する

RolesGuardは問題なく動作しますが、まだ改善の余地があります。というのも、Guardには実行コンテキストという重要な機能があるからです。現状のRolesGuardは、ロールについて関与せず、それぞれのハンドラでどのようなロールが許可されているか把握していません。CatsControllerがルートごとに異なる権限を求める場合を考えてみましょう。管理者ユーザのみ使用可能なものもあれば、誰でも使用可能なものもあるでしょう。柔軟性と再利用性を保ったまま、どのようにしてルートに対してロールを割り当てれば良いでしょうか?

そこで、カスタムメタデータの出番です。(詳しくは、ここをご参照ください)Nestでは、カスタムメタデータをルートハンドラに付与するために、@SetMetadata()というデコレータを用意しています。メタデータにroleのデータを提供することで、よりスマートなGuardを実装することが可能になります。では、実際に@SetMetadataを使ってみましょう。

cats.controller.ts
@Post()
@SetMetadata('roles', ['admin'])
async create(@Body() createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);
}

@SetMetadataデコレータは、@nestjs/commonパッケージからインポートしてください。

上のコードでは、create()メソッドにrolesというメタデータを付与しています。rolesがキーで、['admin']が値です。これは問題なく動作しますが、ルートに対して直接@SetMetadata()を使用するのは推奨されません。代わりに、以下のようにデコレータを定義します。

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

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

こうすることで、可読性が上がり、より強く型付けできます。では、作成した@Roles()デコレータをcreate()メソッドに使用してみましょう。

cats.controller.ts
@Post()
@Roles('admin')
async create(@Body() createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);
}

仕上げ

作成したRolesRolesGuardで利用しましょう。現状では、RolesGuardは常にtrueを返し、全てのリクエストは許可されています。これから実装したいのは、リクエストしたユーザのロールと、ルートにアクセスする許可のあるロールを比較し、それに基づいて返す値を変える仕組みです。ルートに権限のあるロールのメタデータにアクセスするために、@nestjs/coreパッケージに含まれているReflectorというヘルパークラスを使用します。

roles.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';

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

  canActivate(context: ExecutionContext): boolean {
    const roles = this.reflector.get<string[]>('roles', context.getHandler());
    if (!roles) {
      return true;
    }
    const request = context.switchToHttp().getRequest();
    const user = request.user;
    return matchRoles(roles, user.roles);
  }
}

Node.jsでは、認可済みのユーザをrequestオブジェクトに付与するのが一般的です。したがって、上のサンプルコードでも、request.userにユーザのインスタンスとロールが含まれている前提になっています。実際のプロダクトにおいても、認証のためのGuardやミドルウェアでこのような紐付けを行う場合があるでしょう。詳しくは、認証の章を参照してください。

matchRoles()関数のロジックは、シンプルなものでも、複雑なものでも構いません。このサンプルは、リクエストとレスポンスのサイクルにどうGuardが関わるかを説明することに重点を置いているので、matchRoles()関数の実装については扱いません。

Reflectorをコンテキストに応じて使用する方法の詳細については、実行コンテキストの章のReflectionとメタデータの章でReflectionとメタデータを参照してください。

ユーザがエンドポイントに対して必要な権限を満たしていなかった場合、Nestは自動的に以下のようなレスポンスを返します。

{
  "statusCode": 403,
  "message": "Forbidden resource",
  "error": "Forbidden"
}

内部的には、Guardがfalseを返したとき、フレームワークはForbiddenExceptionを投げています。他のレスポンスを返したい場合は、独自に例外を投げてください。例えば、以下のようにします。

throw new UnauthorizedException();

Guardで投げられた例外は、Exceptionレイヤで処理されます。ここでいうExceptionレイヤとは、グローバルなExceptionレイヤと、現在のコンテキストに適用されたExceptionレイヤのことです。

認可を実装した実際のサンプルをお探しなら、認可の章をご参照ください。

修正依頼について

誤字・脱字や翻訳ミスなどを見つけた場合は、ぜひご連絡ください。
もっといい翻訳の仕方がある、等でも嬉しいです。
GitHubでのプルリクや、TwitterのDMでも受け付けております。

https://twitter.com/Michin0suke
GitHubで編集を提案

Discussion

ログインするとコメントできます