Guardとは

Guardとは

Guardとは、@Injectable()デコレータでアノテーションされたクラスで、CanActivateインターフェイスを実装しています。
Guardは一つの責任を持ちます。それらは、実行時に存在する特定の条件に応じて、与えられたリクエストがルートハンドラによって処理されるかどうかを決定します。これは認可(authorization)と言います。
認可は、従来のExpressではミドルウェアによって処理されていました。(認証は、通常これと連携しています)トークンの検証やリクエストオブジェクトへのプロパティのアタッチなどは、特定のルートコンテキスト(およびそのメタデータ)と強く結びついていないため、ミドルウェアは認証に適した選択だと言えます。
しかし、ミドルウェアはその性質上、頭が悪いのです。next()関数を呼び出した後にどのハンドラが実行されるかはわかりません。一方、GuardはExecutionContextインスタンスにアクセスできるため、次に何が実行されるかを正確に知ることができます。例外フィルタやパイプ、インターセプターのように、リクエスト/レスポンスサイクルの適切なタイミングで処理ロジックを挿入できるように設計されており、それを宣言的に実行することができます。これは、あなたのコードをDRYかつ宣言的に保つのに役立ちます。
メモ
ガードはすべてのミドルウェアの後、インターセプターやパイプの前に実行されます。

認可(Authorization)に使う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()関数内のロジックは、必要なだけシンプルにすることも 洗練されたものにすることもできます。この例の主なポイントは、ガードがリクエスト/レスポンスサイクルにどのように適合するかを示すことです。
すべてのガードはcanActivate()関数を実装しなければなりません。この関数は、現在のリクエストが許可されているかどうかを示すbooleanを返さなければなりません。この関数は、同期または非同期(PromiseまたはObservable経由)でレスポンスを返すことができます。Nestはその返り値を用いて、次のアクションを制御します。
  • trueを返す場合は、リクエストがそのまま進行されます。
  • falseを返す場合は、リクエストを拒否します。

実行コンテキスト

canActivate()関数は、ExecutionContextのインスタンスを 1 つの引数として受け取ります。ExecutionContextArgumentsHostを継承しています。
ArgumentsHostを拡張することで、ExecutionContextは現在の実行プロセスに関する追加の詳細を提供するいくつかの新しいヘルパーメソッドも追加しています。これらのメソッドは、コントローラやメソッド、実行コンテキストにまたがって動作する、より一般的な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をバインドする

パイプや例外フィルタと同様に、Guardもコントローラ、メソッドやグローバルのいずれかにスコープできます。以下の例では、@UseGuards()デコレータを活用してコントローラスコープのGuardを設定します。このデコレータは、単一の引数あるいはカンマで区切られた引数のリストを受け取ることができます。
これによって、一度の宣言で適切なGuardのセットを簡単に適用できます。
@Controller('cats')
@UseGuards(RolesGuard)
export class CatsController {}
メモ
@UseGuards()デコレータは@nestjs/commonパッケージからインポートできます。
上記では、インスタンスの代わりにRolesGuardの型を渡して、インスタンス化の責任をフレームワークに委ねて依存性注入を可能にしました。パイプや例外フィルタのように、インプレイス異スタンスを通すこともできます。
@Controller('cats')
@UseGuards(new RolesGuard())
export class CatsController {}
上記の構文では、このコントローラで宣言されているすべてのハンドラにGuardを添付しています。もし特定のメソッドにのみGuardを適用させたい場合は@UseGuards()デコレータをメソッドレベルで適用します。
const app = await NestFactory.create(AppModule);
app.useGlobalGuards(new RolesGuard());
注意
ハイブリッドアプリの場合、useGlobalGuards()メソッドはデフォルトでゲートウェイとマイクロサービスのガードをセットアップしません(この動作を変更する方法については、ハイブリッドアプリを参照してください)。標準的な(ハイブリッドではない)マイクロサービス・アプリケーションの場合、useGlobalGuards()はガードをグローバルにマウントします。
グローバルガードは、アプリケーション全体、コントローラやルートハンドラで使用します。依存性注入の観点からは、どのモジュールの外からでも登録できるグローバルガードはどのモジュールのコンテキスト外でも行なわれるので、依存性を注入できません。この問題を解決するために、以下のような構文で任意のモジュールから直接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 {}
メモ
この方法でガードの依存性注入を行う場合、この構造が採用されるモジュールに関係なく、ガードは実際にはグローバルであることに注意してください。ガード(上の例ではRolesGuard)が定義されているモジュールを選んでください。また、useClassはカスタムプロバイダ登録を扱う唯一の方法というわけではありません。

ハンドラごとの役割の設定

RolesGuardは機能していますが、まだあまりスマートではありません。ガードの最も重要な機能である実行コンテキストをまだ利用していないのです。ロールについて、また各ハンドラでどのロールが許可されているかについては、まだわかっていません。例えば、CatsControllerはルートごとに異なる権限体系を持つことができる。例えばCatsControllerは、ルートごとに異なる権限体系を持つことができます。あるものは管理者だけが利用でき、別のものは誰でも利用できるようになるかもしれません。では、どのようにすればロールとルートを柔軟に再利用できるようになるのでしょうか?
そこで、カスタムメタデータが活躍します。Nestでは、@SetMetadata()デコレータを使ってカスタムメタデータをルートハンドラにアタッチする機能があります。このメタデータは、スマートガードが意思決定を行うために必要な、不足しているロールデータを提供します。それでは、@SetMetadata()の使い方を見てみましょう。
@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);
}

まとめ

ここで、RolesGuardに戻りこれを結びつけてみましょう。現在、これはすべてのケースで単にtrueを返し、すべてのリクエストの処理を許可しています。現在のユーザーに割り当てられているロールと、現在処理中のルートで必要とされる実際のロールを比較して、条件付きの戻り値を作りたいと思います。ルートのロール(カスタムメタデータ)にアクセスするために、Reflectorヘルパークラスを使用します。これはフレームワークによってすぐに提供され、@nestjs/coreパッケージから公開されます。
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にユーザインスタンスと許可されたロールが含まれると仮定しています。あなたのアプリでは、おそらくカスタムの認証ガード(またはミドルウェア)でその関連付けを行うことになるでしょう。
注意
matchRoles()関数内のロジックは、必要なだけ単純でも洗練されていてもかまいません。この例の主なポイントは、Guardがリクエスト・レスポンスサイクルにどのように適合するかを示すことです。
十分な権限を持たないユーザがエンドポイントを要求した場合、NestJSは自動的に以下のレスポンスを返します。
{
"statusCode": 403,
"message": "Forbidden resource",
"error": "Forbidden"
}
裏側では、ガードがfalseを返すと、フレームワークが ForbiddenExceptionを投げることに注意してください。もし、別のエラーレスポンスを返したい場合は、独自の例外を投げる必要があります。
throw new UnauthorizedException();