NestJSでGuardを使って認可を実装する
NestJS における Guard
Guard とは、@Injectable()デコレータでアノテーションされたクラスで、CanActivate インターフェイスを実装しています。
Guard は一つの責任を持ちます。それらは、実行時に存在する特定の条件に応じて、与えられたリクエストがルートハンドラによって処理されるかどうかを決定します。これは認可(authorization)と言います。
Nest.js におけるGuard
は実際には次の条件を全て満たすようなものです:
- クラスである
-
@Injectable()
デコレーターでアノテーションされている -
CanActivate
というインターフェースをimplements
している -
canActivate
という、ExecutionContext
型を引数にとり、同期または非同期で boolean 値(true または false)を返すメソッドを実装している
つまり、以下のような内容になります:
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);
}
}
このようなテンプレートは以下のnest cli
コマンドによっても作成できます:
# nest g gu auth などでも可能。詳しくは nest --help を参照のこと。
nest genarate guard auth
そして次のように使います:
@Controller('cats')
@UseGuards(RolesGuard) // CatsControllerのハンドラー全体をGuard
export class CatsController {
// いろいろなハンドラーたち
}
特定のハンドラーだけを Guard することもできます:
@Controller('cats')
export class CatsController {
@UseGuards(RolesGuard) // 直下のハンドラーだけをguard
@Get()
async getCats() {
return "cats を getするよ"
}
}
canActivate の引数である context: ExecutionContext について
Guard のすごいところはcanActivate
の引数のcontext
から、「guard がどこにバインドされているか」を知ることができることです
もっと詳しく見ましょう。ExecutionContext 型は以下のような実装になっています:
// ArgumentsHostをextendsしているので、ハンドラーのリクエストオブジェクトなどにもアクセスできる
export interface ExecutionContext extends ArgumentsHost {
// 現在のハンドラーが属するコントローラーのクラスの型を返す
getClass<T = any>(): Type<T>;
// リクエストパイプラインの中で、次に起動されるハンドラーへの参照を返す
getHandler(): Function;
}
SetMetadata で特定のハンドラーに情報を付与する
コントローラーの中で、このメソッドは管理者だけにしか実行させたくない、みたいな状況があると思います。
その場合は、Guard
に加えてSetMetadata
を組み合わせることでいい感じに実装できます。
例えば、CatsController の中で、create メソッドは管理者権限があるユーザーにしか実行できないようにしたいとします。そのようなときは、以下のようにします:
@Controller("cats")
@UseGuards(RolesGuard) // CatsControllerのハンドラー全体をGuard
export class CatsController {
@Post()
@SetMetadata("roles", ["admin"]) // ハンドラーに対して情報を付与する
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
}
そして、RolesGuard
において、ユーザーの role と、ハンドラーの role を検証し、実行可能かどうかを確認します。
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()); // ここでhandlerのメタデータを取得している
if (!roles) {
return true;
}
const request = context.switchToHttp().getRequest(); // ここでハンドラーにきたリクエストを取得している
const user = request.user;
return matchRoles(roles, user.roles); // matchRolesの実装はいい感じにやる
}
}
重要なのは以下の点です:
- context.getHandler()でハンドラーの情報を取得している
- reflector というものを使ってハンドラーのメタデータを取得している
- context.switchToHttp().getRequest()でハンドラーに入ってくるリクエストオブジェクトを取得している
なお、十分な権限を持たないユーザーがエンドポイントにリクエストした場合、NestJS は自動的に以下のレスポンスを返してくれます:
{
"statusCode": 403,
"message": "Forbidden resource",
"error": "Forbidden"
}
参考
Discussion