haileyjpark

[Nest.js] Nest.js의 Guard (1) - ArgumentsHost, ExecutionContext 본문

Nest.js

[Nest.js] Nest.js의 Guard (1) - ArgumentsHost, ExecutionContext

개발하는 헤일리 2023. 4. 9. 23:31
반응형

Guard

Nest.js Guard

guard는 @Injectable() 데코레이터가 달린, CanActivate 인터페이스를 구현하는 클래스입니다.

가드가 가진 하나의 역할은, 실행 시점에 존재하는 권한, 역할, ACL(Access Control List, 접근 제어 목록) 등의 조건에 따라 현재 요청을 라우트 핸들러가 처리할 지 여부를 결정합니다.

이를 일반적으로 권한 부여(Authorization)라고 하는데, 권한 부여와 관련한 인증(Authentication)과 함께 대부분의 애플리케이션에서는 미들웨어가 처리합니다.

 

하지만 미들웨어는 next() 함수를 호출한 후 어떤 핸들러가 실행될 지 알지 못합니다.

반면에 가드는 ExecutionContext 인스턴스에 액세스할 수 있으며, 따라서 다음에 실행될 것이 정확히 무엇인지 알고 있습니다. exception filters, pipes, interceptors와 마찬가지로, 요청/응답 주기의 정확한 지점에 처리 로직을 끼워넣고, 선언적으로 수행할 수 있도록 설계되었습니다.

 

가드는 모든 미들웨어 이후에 실행되지만, pipes 또는 interceptors 이전에 실행됩니다.

 

auth.guard.tsJS

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.js 공식문서의 Auth guard 예시입니다.

각 가드는 반드시 canActivate() 함수를 구현해야 합니다. 이 함수는 현재 요청이 허용되는지 여부를 나타내는 boolean 값을 반환해야 합니다. 동기적으로 또는 비동기적으로 (Promise 또는 Observable을 통해) 응답을 반환할 수 있습니다. Nest는 반환 값에 따라 다음 동작을 제어합니다.

  • true를 반환하면 요청이 처리됩니다.
  • false를 반환하면 Nest가 요청을 거부합니다.

canActivate() 함수는 ExecutionContext 인스턴스라는 단일 인자를 사용합니다. ExecutionContext는 ArgumentsHost에서 상속됩니다. 

 

Execution Context

Nest는 여러 애플리케이션 컨텍스트(예 : Nest HTTP 서버 기반, 마이크로서비스 및 웹소켓 애플리케이션 컨텍스트)에서 작동하는 애플리케이션을 쉽게 작성할 수 있도록 도와주는 여러 유틸리티 클래스를 제공합니다.

이러한 유틸리티는 현재 실행 컨텍스트에 대한 정보를 제공하며, 넓은 범위의 컨트롤러, 메서드 및 실행 컨텍스트에서 작동할 수 있는 일반적인 가드, 필터 및 인터셉터를 구축하는 데 사용할 수 있습니다.

 

ArgumentsHost class

ArgumentsHost 클래스는 핸들러에 전달되는 인자를 가져올 메서드들을 제공합니다. 인자를 가져올 적절한 컨텍스트(HTTP, RPC (마이크로서비스) 또는 웹소켓)를 선택할 수 있습니다.

 

ArgumentsHost는 핸들러의 인수를 캡슐화하는 추상화 역할을 합니다.

예를 들어, HTTP 서버 애플리케이션(@nestjs/platform-express를 사용하는 경우)의 경우 host 객체는 request, response, next] 배열을 캡슐화합니다. 여기서 request는 요청 객체, response는 응답 객체이며, next는 애플리케이션의 요청-응답 주기를 제어하는 함수입니다.

GraphQL 애플리케이션의 경우 host 객체는 [root, args, context, info] 배열을 포함하고, websocket의 경우 clientdata가 있습니다.

 

if (host.getType() === 'http') {
  // do something that is only important in the context of regular HTTP requests (REST)
} else if (host.getType() === 'rpc') {
  // do something that is only important in the context of Microservice requests
} else if (host.getType<GqlContextType>() === 'graphql') {
  // do something that is only important in the context of GraphQL requests
}

위와 같이, 제너럴하게 사용할 수 있는 guards, filters, interceptors를 만들 때 context에 따라 분기처리할 수 있도록 getType이라는 메소드도 제공합니다.

 

const [req, res, next] = host.getArgs();

위의 메서드는 핸들러로 패스되었던 인자들을 가져오는 메서드입니다.

 

/**
 * Switch context to RPC.
 */
switchToRpc(): RpcArgumentsHost;
/**
 * Switch context to HTTP.
 */
switchToHttp(): HttpArgumentsHost;
/**
 * Switch context to WebSockets.
 */
switchToWs(): WsArgumentsHost;
const ctx = host.switchToHttp();
const request = ctx.getRequest<Request>();
const response = ctx.getResponse<Response>();

위의 예시는 호스트 객체의 유틸리티 메서드 중 적절한 애플리케이션 컨텍스트로 전환하기 위해 사용할 수 있는 메서드를 사용하여 코드를 더 견고하고 재사용 가능하도록 만들 수 있는 방법을 보여줍니다.

 

host.switchToHttp() 헬퍼 호출은 HTTP 애플리케이션 컨텍스트에 적합한 HttpArgumentsHost 객체를 반환합니다. HttpArgumentsHost 객체에는 원하는 객체를 추출하는 데 유용한 두 가지 메서드가 있습니다.

이 경우 Express 타입 어설션을 사용하여 네이티브 Express 타입 객체를 반환합니다.

 

 

ExecutionContext class

 

ExecutionContext은 ArgumentsHost를 확장하며, 현재 실행 프로세스에 대한 추가적인 세부 정보를 제공합니다.

ArgumentsHost와 마찬가지로, 가드의 canActivate() 메서드나 인터셉터의 intercept() 메서드와 같이 ExecutionContext의 인스턴스를 필요로 하는 곳에서 Nest는 이를 제공합니다.

 

ExecutionContext는 다음과 같은 메서드를 제공합니다. :

export interface ExecutionContext extends ArgumentsHost {
  /**
   * Returns the type of the controller class which the current handler belongs to.
   */
  getClass<T>(): Type<T>;
  /**
   * Returns a reference to the handler (method) that will be invoked next in the
   * request pipeline.
   */
  getHandler(): Function;
}

 

getHandler() 메서드는 호출될 핸들러에 대한 참조를 반환합니다.

getClass() 메서드는 이 특정 핸들러가 속한 컨트롤러 클래스의 유형을 반환합니다.

예를 들어, HTTP 컨텍스트에서 현재 처리 중인 요청이 create() 메서드에 바인드된 POST 요청인 경우, getHandler()는 create() 메서드에 대한 참조를 반환하고 getClass()는 CatsController 유형 (인스턴스가 아님)을 반환합니다.

const methodKey = ctx.getHandler().name; // "create"
const className = ctx.getClass().name; // "CatsController"

 

현재 클래스와 핸들러 메서드에 대한 참조에 액세스할 수 있는 기능은 큰 유연성을 제공합니다.

가장 중요한 것은 가드 또는 인터셉터 내에서 @SetMetadata() 데코레이터를 통해 설정된 메타데이터에 액세스할 수 있는 기회를 제공한다는 것입니다. 이 사용 사례에 대해 아래에서 살펴볼 수 있습니다.

 

Reflection and metadata

Nest는 @SetMetadata() 데코레이터를 통해 라우트 핸들러에 사용자 정의 메타데이터를 추가하는 기능을 제공합니다. 그런 다음 클래스 내부에서 이 메타데이터에 액세스하여 특정 결정을 내릴 수 있습니다.

cats.controller.tsJS

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

 

위의 코드에서는 roles 메타데이터(roles는 메타데이터 키, ['admin']은 관련 값)를 create() 메서드에 첨부했습니다.
이 방법은 작동하지만, 라우트에서 @SetMetadata()를 직접 사용하는 것은 좋은 방법이 아닙니다.

대신 아래와 같이 사용자 지정 데코레이터를 만드는 방법이 권장됩니다.:

 

roles.decorator.tsJS

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

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

 

이 방법은 훨씬 깨끗하고 가독성이 좋으며, 강력한 타입 지정이 가능합니다.

이제 이 방법으로 사용자 지정 @Roles() 데코레이터를 만들어 create() 메서드를 꾸밀 수 있습니다.

 

cats.controller.tsJS

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

 

라우트의 Roles(사용자 정의 메타데이터)에 액세스하기 위해 Reflector 도우미 클래스를 사용합니다.

이 클래스는 프레임워크에서 기본적으로 제공되며 @nestjs/core 패키지에서 import됩니다.

Reflector는 일반적인 방법으로 클래스에 주입될 수 있습니다.

 

roles.guard.tsJS

@Injectable()
export class RolesGuard {
  constructor(private reflector: Reflector) {}
}
const roles = this.reflector.get<string[]>('roles', context.getHandler());

핸들러 metadata를 읽어오기 위해, 위와 같이 get() 메서드를 사용합니다.

 

 

Reflector의 get 메소드는 메타데이터 key와 컨텍스트(데코레이터 대상)를 전달하여 메타데이터에 쉽게 액세스 할 수 있게 해줍니다.

이 예제에서 지정된 키는 'roles'이며(위의 roles.decorator.ts 파일과 SetMetadata() 호출 참조),

 

컨텍스트는 context.getHandler() 호출로 제공되어 현재 처리 중인 라우트 핸들러에 대한 메타데이터를 추출하게 됩니다.

(getHandler()는 라우트 핸들러 함수에 대한 참조를 제공)

 

또는, 모든 라우트에 적용되는 컨트롤러 레벨의 메타데이터를 적용해서 컨트롤러를 구성할 수 있습니다.

cats.controller.tsJS

@Roles('admin')
@Controller('cats')
export class CatsController {}

 

이 경우, 컨트롤러 메타데이터를 추출하려면 context.getHandler() 대신 context.getClass()를 두 번째 인수로 전달하여(컨트롤러 클래스를 메타데이터 추출의 컨텍스트로 제공하기 위해) 메타데이터를 추출합니다.

 

roles.guard.tsJS

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

 

여러 레벨에서 메타데이터를 제공할 수있는 능력을 고려해볼 때, 여러 컨텍스트에서 메타데이터를 추출하고 병합해야 하는 경우가 있을 수 있습니다.

Reflector 클래스는 이를 돕기위해 사용되는 두 가지 유틸리티 메소드를 제공합니다.

이러한 메소드는 컨트롤러 및 메소드 메타데이터를 한 번에 추출하고 다양한 방법으로 병합합니다.

 

 

다음 예제는 두 레벨에서 'roles' 메타데이터를 제공한 경우입니다.

cats.controller.tsJS

@Roles('user')
@Controller('cats')
export class CatsController {
  @Post()
  @Roles('admin')
  async create(@Body() createCatDto: CreateCatDto) {
    this.catsService.create(createCatDto);
  }
}

 

기본 역할로 'user'를 지정하고 특정 메소드에 대해 선택적으로 덮어 씌우려는 경우, 일반적으로 getAllAndOverride() 메소드를 사용할 것입니다.

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

 

위의 메타데이터를 가진 create() 메서드의 컨텍스트에서 실행되는 이 코드를 가진 가드는 roles가 ['admin']인 것으로 결과를 반환합니다.

둘 모두에 대한 메타데이터를 얻고 병합하려면 아래와 같이 getAllAndMerge() 메소드를 사용할 수 있습니다.

 

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

 

이렇게하면 roles가 ['user', 'admin']인 것으로 결과가 반환됩니다.

이 병함 메서드의 경우, 첫 번째 인자로 메타데이터 키를 전달하고, 두 번째 인자로 메타데이터 타겟 컨텍스트 array (getHandler() 또는 getClass()를 전달합니다.

 

 

 

 

 

참고자료

https://docs.nestjs.com/fundamentals/execution-context

https://docs.nestjs.com/guards

 

Documentation | NestJS - A progressive Node.js framework

Nest is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, is built with TypeScript and combines elements of OOP (Object Oriented Programming), FP (Functional Programming), and FRP (Functional Rea

docs.nestjs.com

 

Documentation | NestJS - A progressive Node.js framework

Nest is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, is built with TypeScript and combines elements of OOP (Object Oriented Programming), FP (Functional Programming), and FRP (Functional Rea

docs.nestjs.com