NestJS ガード

ガード

ガードは@Injectable()デコレータで注釈が付けられたクラスです。ガードを使う場合は、CanActivateインターフェイスを実装する必要があります。

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

f:id:adrenaline2017:20200803083925p:plain

ガードはクライアントのHTTPリクエストがルートハンドラによって処理されるかどうかを決定する役割を持っています。これまでは、アクセス制限のロジックは主にミドルウェア内部にありました。トークン(ソースコードを構成する最小単位)の検証やプロパティーのリクエストオブジェクトへのアタッチなどは、特定のルートと強く結びついていないので問題はありません。

しかし、ミドルウェアは性質上、next()関数を呼び出した後にどのハンドラが実行されるかはわかりません。一方、ガードはExecutionContextインスタンスへのアクセス権を持っており、評価される内容が正確にわかります。

ヒント ガードは各ミドルウェアの後、パイプの前に実行されます。

認可ガード

一番のガードのユースケースの1つは認証ロジックです。特定のルートは、呼び出し元に十分な権限がある場合にのみ使用できるようにする必要があるためです。作成する予定の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()関数を提供します。 ガードは、プロミスまたはオブザーバブルを介して、そのブール値の応答を同期的または非同期的に返すことができます。 戻り値は、nestの動作を制御します。

trueを返すと、ユーザー呼び出しが処理されます。 falseを返した場合、Nestは現在処理中のリクエストを無視します。 canActivate()関数は、単一の引数であるExecutionContextインスタンスをとります。 ExecutionContextはArgumentsHostから継承しています(ここで最初に言及します)。 ArgumentsHostは、元のハンドラに渡された引数のラッパーであり、アプリケーションのタイプに基づいて、フードの下に異なる引数配列を含みます。

export interface ArgumentsHost {
  getArgs<T extends Array<any> = any[]>(): T;
  getArgByIndex<T = any>(index: number): T;
  switchToRpc(): RpcArgumentsHost;
  switchToHttp(): HttpArgumentsHost;
  switchToWs(): WsArgumentsHost;
}

ArgumentsHostは、基本配列から正しい引数を選ぶのに役立つ一連の便利なメソッドを提供します。 言い換えれば、ArgumentsHostは引数の配列だけではありません。 たとえば、HTTPアプリケーションコンテキスト内でガードが使用されている場合、ArgumentsHostには[request、response]配列が含まれます。 ただし、現在のコンテキストがWebソケットアプリケーションの場合、この配列は[client、data]と等しくなります。 この設計の決定により、最終的に対応するハンドラに渡されるすべての引数にアクセスできます。

ExecutionContextはもう少しです。 それはArgumentsHostを拡張するだけでなく、現在の実行プロセスの詳細を提供します。

export interface ExecutionContext extends ArgumentsHost {
  getClass<T = any>(): Type<T>;
  getHandler(): Function;
}

getHandler()は現在処理中のハンドラへの参照を返しますが、getClass()はこのハンドラが属するControllerクラスの型を返します。 他の言葉を使用すると、ユーザーがCatsController内で定義され登録されているcreate()メソッドを指している場合、getHandler()はcreate()メソッドとgetClass()への参照を返します。 インスタンス)。

役割ベースの認証

もう少し詳細な例がRolesGuardです。 このガードは、特定の役割を持つユーザーのみにアクセスを許可します。 基本的なガードテンプレートから始めます:

roles.guard.ts JS

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()デコレータを使用する必要があります。 このデコレータは無数の引数をとります。つまり、複数のガードを渡してコンマで区切ることができます。

cats.controller.ts JS

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

ヒント @UseGuards()デコレータは、@ nestjs / commonパッケージからインポートされます。 インスタンスの代わりにRolesGuard型を渡し、フレームワークインスタンス化の責任を任せ、依存性注入を可能にしました。 別の利用可能な方法は、直ちに作成されたインスタンスを渡すことです。

cats.controller.ts

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

上記の構成は、このコントローラによって宣言されたすべてのハンドラにガードを添付します。 そのうちの1つだけを制限することに決めたら、メソッドレベルでガードを設定するだけです。 グローバルガードをバインドするには、NestアプリケーションインスタンスのuseGlobalGuards()メソッドを使用します。

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

通知 useGlobalGuards()メソッドは、ゲートウェイとマイクロサービスのガードを設定しません。 グローバルガードは、すべてのコントローラとすべてのルートハンドラに対して、アプリケーション全体で使用されます。 依存性注入に関しては、(上記の例のように)モジュールの外部から登録されたグローバルガードは、いずれのモジュールにも属していないため、依存関係を注入できません。 この問題を解決するには、次の構成を使用して、任意のモジュールから直接ガードを設定できます。

app.module.ts JS

import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';

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

ヒント 代替オプションは、実行コンテキスト機能を使用することです。 また、useClassはカスタムプロバイダの登録を処理する唯一の方法でもありません。 詳細はこちら。

リフレクター

ガードは現在動作していますが、最も重要なガード機能、つまり実行コンテキストを利用していません。

これまでRolesGuardは再利用できません。 どのような役割がハンドラによって処理される必要があるのかをどのように知ることができますか? CatsControllerにはたくさんのものがあります。 管理者だけで利用できるものもあれば誰でも利用できるものもありますが、アクセス権はありません。

そのため、ガードと一緒に、Nestは@ReflectMetadata()デコレータを使用してカスタムメタデータを添付することができます。

cats.controller.ts JS

@Post()
@ReflectMetadata('roles', ['admin'])
async create(@Body() createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);

ヒント @ReflectMetadata()デコレータは、@ nestjs / commonパッケージからインポートされます。

上記の構成では、create()メソッドに対してロールメタデータ(ロールはキーで、['admin']は特定の値)を添付しました。 @ReflectMetadata()を直接使用するのは良い方法ではありません。 代わりに、あなた自身のデコレータを常に作成する必要があります:

roles.decorator.ts

import { ReflectMetadata } from '@nestjs/common';

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

このアプローチははるかにクリーンで読みやすい。 私たちは@Roles()デコレータを持っているので、create()メソッドでそれを使うことができます。

cats.controller.ts

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

よかった。 もう一度RolesGuardに戻ってみましょう。 これはただちに真を返し、これまで要求を進めることができます。 メタデータを反映させるために、Reflectorヘルパークラスを使用します。このヘルパークラスはフレームワークによってそのまま提供され、@ nestjs / coreパッケージから公開されます。

roles.guard.ts

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.userにユーザーインスタンスが含まれていると仮定しています。

Reflectorでは、メタデータを指定されたキーで簡単に反映させることができます。 上記の例では、ルートハンドラ関数への参照であるため、メタデータを反映するためにgetHandler()を使用しました。 コントローラーの反射部分も追加すれば、このガードをさらに一般的にすることができます。 コントローラメタデータを抽出するには、getHandler()関数の代わりにcontext.getClass()を使用する必要があります。

const roles = this.reflector.get<string[]>('roles', context.getClass());

ユーザーが十分な権限なしに/ cats POSTエンドポイントを呼び出そうとすると、Nestは自動的に以下の応答を返します。

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

実際、falseを返すガードはHttpExceptionをスローします。 エンドユーザーに別のエラー応答を返す場合は、例外をスローする必要があります。 その後、この例外は例外フィルタによって捕捉されます。