高瀬博道の技術ブログ

高瀬博道の技術ブログです。

もう2023年になったので、今さらNestJSに入門しようと思う - その15

前回

takasehiromichiex.com

実行コンテクスト

canActivate()メソッドは、ExecutionContextインスタンスという1つの引数をとります。

ExecutionContextは、ArgumentsHostから継承します。

ArgumentsHostを拡張することで、ExecutionContextは、現在の実行プロセスに関する追加の詳細を提供する新しいヘルパメソッドも追加します。

ロールベースの認証

特定の役割を持つユーザにのみアクセスを許可する、より機能的なガードを作成してみましょう。

$ nest g guard guard/roles

以下のファイルが作成されました。

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

@Injectable()
export class RolesGuard implements CanActivate {
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    return true;
  }
}
src/guard/roles/roles.guard.spec.ts
import { RolesGuard } from './roles.guard';

describe('RolesGuard', () => {
  it('should be defined', () => {
    expect(new RolesGuard()).toBeDefined();
  });
});

コントローラにガードをバインドします。

src/cats/cats.controller.ts
import { Body, Controller, Get, Post, UseGuards } from '@nestjs/common';
import { CreateCatDto } from 'src/dto/cat/cat.dto';
import { Cat } from './cats.interface';
import { CatsService } from './cats.service';
import { ValidationPipe } from '../pipe/validation/validation.pipe';
import { RolesGuard } from 'src/guard/roles/roles.guard';

@Controller('cats')
@UseGuards(RolesGuard)
export class CatsController {
    constructor(private catsService: CatsService) {}
    
    @Post()
    create(@Body(new ValidationPipe()) createCatDto: CreateCatDto) {
        this.catsService.create(createCatDto);
    }
    @Get()
    findAll(): Cat[] {
        return this.catsService.findAll();
    }
}

@UseGuards(RolesGuard)を追加しました。

この構成は、コントローラによって宣言された全てのハンドラにガードをアタッチしています。

また、この状態では、また各ハンドラにどのロールが許可されているかわかりません。

そのため、カスタムメタデータを使用します。

@SetMetadata()デコレータを使用して、カスタムメタデータをルートハンドラにアタッチすることができます。

src/cats/cats.controller.ts
import { Body, Controller, Get, Post, SetMetadata, UseGuards } from '@nestjs/common';
import { CreateCatDto } from 'src/dto/cat/cat.dto';
import { Cat } from './cats.interface';
import { CatsService } from './cats.service';
import { ValidationPipe } from '../pipe/validation/validation.pipe';
import { RolesGuard } from 'src/guard/roles/roles.guard';

@Controller('cats')
@UseGuards(RolesGuard)
export class CatsController {
    constructor(private catsService: CatsService) {}
    
    @Post()
    @SetMetadata('roles', ['admin'])
    create(@Body(new ValidationPipe()) createCatDto: CreateCatDto) {
        this.catsService.create(createCatDto);
    }
    @Get()
    findAll(): Cat[] {
        return this.catsService.findAll();
    }
}

@SetMetadata('roles', ['admin'])を設定しました。

ただし、この記述方法は動作しますが、推奨されません。

代わりに、カスタムデコレータを作成します。

$ nest g decorator decorator/roles

以下のファイルが作成されました。

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

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

カスタムデコレータが作成できたので、コントローラにバインドします。

src/cats/cats.controller.ts
import { Body, Controller, Get, Post, UseGuards } from "@nestjs/common";
import { CreateCatDto } from "src/dto/cat/cat.dto";
import { Cat } from "./cats.interface";
import { CatsService } from "./cats.service";
import { ValidationPipe } from "../pipe/validation/validation.pipe";
import { Roles } from "src/decorator/roles/roles.decorator";
import { RolesGuard } from "src/guard/roles/roles.guard";

@Controller("cats")
@UseGuards(RolesGuard)
export class CatsController {
  constructor(private catsService: CatsService) {}

  @Post()
  @Roles("admin")
  create(@Body(new ValidationPipe()) createCatDto: CreateCatDto) {
    this.catsService.create(createCatDto);
  }
  @Get()
  findAll(): Cat[] {
    return this.catsService.findAll();
  }
}

@Roles('admin')を設置しました。

少しシンプルになりました。

最後に、ガードを修正します。

src/guard/roles/roles.guard.ts
import { CanActivate, ExecutionContext, Injectable } 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);
  }
}

function matchRoles(roles: string[], roles1: any): boolean {
  throw new Error("Function not implemented.");
}

カスタムメタデータにアクセスするために、Reflectorヘルパークラスを使用しています。

matchRolesメソッドは、必要に応じて変更することができます。

まとめ

ここまでで、

  • 実行コンテクストについて
  • ロールベースの認証

を学びました。

次回は、インターセプタについて掘り下げようと思います。

コード

今回のコードは、以下に格納しました。

github.com

takasehiromichiex.com