NestJS公式ドキュメント翻訳
原文
Guards| NestJS - A progressive Node.js web framework
ガード
ガードは@Injectable()
デコレータが付けられたクラスです。ガードはCanActivate
インターフェースを実装する必要があります。
ガードには単一責任性があります。実行時に存在する特定の条件(パーミッション、ロール、ACLなど)に応じて、リクエストがルートハンドラーによって処理されるかどうかを決定します。これは認可(authorization)と呼ばれます。認可(や通常はそれと連携する認証(authentication))は通常は、従来のExpressアプリケーションのミドルウェアによって処理されてきました。ミドルウェアは認証に適した選択肢です。トークンの検証やrequest
オブジェクトへのプロパティの添付などが、特定のルートコンテキスト(およびそのメタデータ)と強く結びついていないためです。
しかし、ミドルウェアはその性質上あまり賢くありません。next()
関数を呼び出した後にどのハンドラーが実行されるかはわかりません。一方、ガードはExecutionContext
インスタンスにアクセスできるため、次に実行されるものを正確に把握することができます。例外フィルター、パイプ、インターセプターと同様に、リクエスト/レスポンスサイクルの正確なポイントに処理ロジックを挿入し、宣言的に実行できるように設計されています。これにより、コードをDRYで宣言的に保つことができます。
ガードは各ミドルウェアの後かつインターセプターやパイプの前に実行されます。
認可ガード
前述のように、特定のルートは呼び出し元(通常は特定の認証済みユーザー)が十分な権限を持っている場合にのみ使用できるため、認可はガードの優れたユースケースです。ここで作成するAuthGuard
は認証されたユーザーを想定しています(トークンがリクエストヘッダーに添付されます)。トークンを抽出および検証し、抽出された情報を使用して、リクエストを続行できるかどうか判断します。
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()
関数を実装する必要があります。この関数は現在のリクエストが許可されているかどうかを示すブール値を返す必要があります。レスポンスを同期的または非同期的に(Promise
またはObservable
を介して)返すことができます。 Nestは戻り値を使用して次のアクションを制御します。
-
true
を返した場合、リクエストが処理されます。 -
false
を返した場合、Nestはリクエストを拒否します。
実行コンテキスト
canActivate()
関数はExecutionContext
インスタンスを引数に取ります。ExecutionContext
はArgumentsHost
を継承します。ArgumentsHost
は例外フィルターの章でも出てきました。上記のサンプルでは、先ほど使用したArgumentsHost
で定義された同じヘルパーメソッドを使用して、Request
オブジェクトへの参照を取得しています。このトピックの詳細については、例外フィルターの章のArguments hostのセクションを参照してください。
ArgumentsHost
を継承することにより、ExecutionContext
は現在の実行プロセスに関する詳細情報を提供する新しいヘルパーメソッドも追加します。これらはコントローラ、メソッド、および実行コンテキストで幅広く機能する、より汎用的なガードを構築するのに役立ちます。ExecutionContext
の追加メソッドは次のようになります。
export interface ExecutionContext extends ArgumentsHost {
getClass<T = any>(): Type<T>;
getHandler(): Function;
}
getHandler()
メソッドは呼び出されるハンドラーへの参照を返します。getClass()
メソッドはこの特定のハンドラーが属するController
クラスの型を返します。例えば、現在処理されているリクエストがCatsController
のcreate()
メソッド宛てのPOST
リクエストの場合、getHandler()
はcreate()
メソッドへの参照を返し、getClass()
は(インスタンスではなく)CatsController
型を返します。
ロールベースの認証
特定のロールを持つユーザーのみにアクセスを許可するような機能的なガードを構築しましょう。基本的なガードのテンプレートから始め、次のセクションでそれを構築していきます。今のところ、すべてのリクエストを続行できます。
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;
}
}
ガードのバインディング
パイプや例外フィルターと同様に、ガードはコントローラスコープ、メソッドスコープ、グローバルスコープにすることができます。以下では、@UseGuards()
デコレータを使用してコントローラスコープのガードを設定します。このデコレータは単一の引数、またはコンマ区切りのリストを引数に取ることができます。これにより、1つの宣言で適切な一連のガードを簡単に適用できます。
@Controller('cats')
@UseGuards(RolesGuard)
export class CatsController {}
@UseGuards()
デコレータは@nestjs/common
パッケージからインポートされます。
上記では(インスタンスの代わりに)RolesGuard
型を渡し、フレームワークにインスタンス化の責任を委ね、依存関係の注入を可能にしました。パイプおよび例外フィルターと同様に、インプレースなインスタンスを渡すこともできます。
@Controller('cats')
@UseGuards(new RolesGuard())
export class CatsController {}
この構造はこのコントローラによって宣言されたすべてのハンドラーにガードを適用します。ガードが単一のメソッドにのみ適用されるようにしたい場合は、メソッドレベルで@UseGuards()
デコレータを適用します。
グローバルガードを設定するにはNestアプリケーションインスタンスのuseGlobalGuards()
メソッドを使用します。
const app = await NestFactory.create(AppModule);
app.useGlobalGuards(new RolesGuard());
ハイブリッドアプリの場合、useGlobalGuards()
メソッドはゲートウェイとマイクロサービスのガードを設定しません。 「スタンダード」(非ハイブリッド)マイクロサービスアプリの場合、useGlobalGuards()
はガードをグローバルにマウントします。
グローバルガードはすべてのコントローラとすべてのルートハンドラに対して、アプリケーション全体で使用されます。依存性注入に関しては、モジュールの外部で登録されたグローバルガード(上記の例ではuseGlobalGuards()
)はモジュールのコンテキスト外で行われるため、依存性を注入できません。この問題を解決するには、次の構成を使用して、任意のモジュールから直接ガードを設定します。
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
にはルートごとに異なるパーミッションスキームを設定することができます。管理ユーザーのみが利用できるものもあれば、すべての人が利用できるものもあります。柔軟で再利用可能な方法で、どのようにロールをルートに一致させることができるでしょうか?
ここでカスタムメタデータの出番です。 Nestは@SetMetadata()
デコレータを介してルートハンドラーにカスタムメタデータを添付する機能を提供します。このメタデータは不足しているrole
データを提供し、ガードは処理を行うべきか決めます。それでは@SetMetadata()
の使い方を見てみましょう。
@Post()
@SetMetadata('roles', ['admin'])
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
@SetMetadata()
デコレータは@nestjs/common
パッケージからインポートされます。
上記の構成で、roles
メタデータ(roles
はキー、['admin']
は特定の値)をcreate()
メソッドに添付しました。これは機能しますが、ルートで@SetMetadata()
を直接使用することはお勧めできません。代わりに、以下のように独自のデコレータを作成しましょう。
import { SetMetadata } from '@nestjs/common';
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
このアプローチはより簡潔で読みやすく、強く型付けされています。カスタムの@Roles()
デコレータができたので、それを使用してcreate()
メソッドをデコレートできます。
@Post()
@Roles('admin')
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
まとめ
少し戻って、これをRolesGuard
と結び付けましょう。現在、すべての場合でtrue
を返すだけなのですべてのリクエストを続行してしまいます。現在のユーザーに割り当てられているロールと処理中の現在のルートに必要な実際のロールとの比較に基づいて、戻り値を条件付きにしましょう。ルートのロール(カスタムメタデータ)にアクセスするため、Reflector
ヘルパークラスを使用します。これはフレームワークによって提供され、@nestjs/core
パッケージから公開されます。
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';
import { Reflector } from '@nestjs/core';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private readonly 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;
const hasRole = () => user.roles.some((role) => roles.includes(role));
return user && user.roles && hasRole();
}
}
node.jsの世界では、承認されたユーザーをrequest
オブジェクトにアタッチするのが一般的です。したがって、上記のサンプルコードでは、request.user
にユーザーインスタンスと許可されたロールが含まれていると想定しています。一般的にアプリでは、おそらくカスタム認証ガード(またはミドルウェア)でその関連付けを行います。
Reflector
クラスを使用すると、指定されたキーでメタデータに簡単にアクセスできます(上記の場合、キーは'roles'
です。roles.decorator.ts
ファイルとそこで行われたSetMetadata()
呼び出しを参照してください)。上記の例では、現在処理されているリクエストメソッドのメタデータを抽出するためにcontext.getHandler()
を渡しました。getHandler()
はルートハンドラー関数への参照を提供することを忘れないでください。
コントローラメタデータを抽出し、それを使用して現在のユーザーロールを決定することにより、このガードをより一般的にすることができます。コントローラのメタデータを抽出するにはcontext.getHandler()
ではなくcontext.getClass()
を渡します。
const roles = this.reflector.get<string[]>('roles', context.getClass());
権限が不十分なユーザーがエンドポイントを要求すると、Nestは自動的に次のレスポンスを返します。
{
"statusCode": 403,
"message": "Forbidden resource"
}
裏でガードがfalse
を返すときフレームワークはForbiddenException
をスローします。別のエラーレスポンスを返す場合は、独自の例外をスローする必要があります。例えば:
throw new UnauthorizedException();
ガードによってスローされたすべての例外は、例外レイヤー(グローバル例外フィルターおよび現在のコンテキストに適用されるすべての例外フィルター)によって処理されます。