8. Guards

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

가드의 책임은 단 하나 입니다 . 런타임에 존재하는 특정 조건(예: 권한, 역할, ACL 등)에 따라 주어진 요청이 경로 핸들러에 의해 처리되는지 여부를 결정한다. 이를 종종 '인가(Authorization)' 라고 합니다.  인가( 이와 비슷한 인증(Authentication) )은 일반적으로 기존 Express 애플리케이션의 미들웨어 에 의해 처리된다 . 미들웨어는 인증을 위한 훌륭한 선택이다. 토큰 유효성 검사 및 객체에 속성 연결과 같은 작업은 request특정 경로 컨텍스트(및 해당 메타데이터)와 강력하게 연결되지 않기 때문이다.

그러나 미들웨어는 본질적으로 멍청하다. next() 함수를 호출한 후 어떤 핸들러가 실행될지 알 수 없다. 반면에 가드는 ExecutionContext 인스턴스에 액세스할 수 있으므로 다음에 실행될 내용을 정확히 알 수 있다. 예외 필터, 파이프 및 인터셉터와 마찬가지로 요청/응답 주기의 정확한 지점에 처리 논리를 삽입하고 선언적으로 그렇게 할 수 있도록 설계되었고 코드를 DRY하고 선언적으로 유지하는 데 도움이 된다.

 

힌트) 가드는 모든 미들웨어 이후에 실행되지만 인터셉터나 파이프 이전에 실행됩니다.

 

인증 가드

언급한 바와 같이 인증은 호출자(일반적으로 특정 인증된 사용자)에게 충분한 권한이 있는 경우에만 특정 경로를 사용할 수 있어야 하기 때문에 가드에 대한 좋은 사례다. 이제 우리가 구축할 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() 기능을 구현해야 한다. 이 함수는 현재 요청이 허용되는지 여부를 나타내는 부울 값을 반환해야 한다. 동기식 또는 비동기식( Promise또는 Observable 를 통해 )으로 응답을 반환할 수 있다. Nest는 반환 값을 사용하여 다음 작업을 제어한다.

  • true 을 반환하면 요청이 처리된다.
  • false 를 반환하면 Nest는 요청을 거부한다.

 

실행 컨텍스트

 canActivate()함수는 ExecutionContext 인스턴스를 단일 인수로 사용한다. ExecutionContext 은  ArgumentsHost 을 상속한다 . ArgumentsHost 는 이전에 예외 필터 장에서 살펴보았다 . 위 샘플에서는 Request 개체에 대한 참조를 가져오기 위해 이전에 사용한 것과 동일한 헬퍼 메서드를 사용하고 있다 . 이 주제에 대한 자세한 내용은 Exception filters 장의  아규먼트 호스트 섹션을 다시 참조할 수 있다 .

ArgumentsHost를 확장 하면,  ExecutionContext 에 현재 실행 프로세스에 대한 추가 세부 정보를 제공하는 몇 가지 새로운 헬퍼 메서드도 추가된다. 이러한 세부 정보는 광범위한 컨트롤러, 메서드 및 실행 컨텍스트 집합에서 작동할 수 있는 보다 일반적인 가드를 구축하는 데 도움이 될 수 있다. 여기 에서 ExecutionContext에 대해 자세히 알아볼 수 있다.

 

역할-기반 인증

특정 역할을 가진 사용자에게만 액세스를 허용하는 보다 기능적인 가드를 구축해 보자. 기본 가드 템플릿부터 시작하여 다음 섹션에서 이를 기반으로 구축한다. 현재로서는 모든 요청을 진행할 수 있다.

// roles.guard.ts

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() 데코레이터를 사용하여 컨트롤러 범위 가드를 설정했다. 이 데코레이터는 단일 인수 또는 쉼표로 구분된 인수 목록을 사용할 수 있다. 이를 통해 하나의 선언으로 적절한 가드 세트를 쉽게 적용할 수 있다.

@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()  예 참조)는 종속성을 주입할 수 없다. 이는 모듈의 컨텍스트 외부에서 수행되기 때문이다. 이 문제를 해결하려면 다음 구성을 사용하여 모든 모듈에서 직접 가드를 설정할 수 있다.

// app.module.ts

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는 정적 메서드 Reflector#createDecorator 를 통해 생성된 데코레이터 또는 내장 데코레이터 @SetMetadata()  를 통해 경로 핸들러에 맞춤 메타데이터를 첨부하는 기능을 제공한다.

 

예를 들어 메타데이터를 핸들러에 첨부하는 Reflector#createDecoratorReflector 메서드를 사용하여 @Roles() 데코레이터를 만들어 보겠다 . 프레임워크에 의해 즉시 제공되고 @nestjs/core 패키지에서 노출된다.

// roles.decorator.ts

import { Reflector } from '@nestjs/core';

export const Roles = Reflector.createDecorator<string[]>();

여기서 Roles  데코레이터는 string[] 유형의 단일 인수를 취하는 함수다.

이제 이 데코레이터를 사용하려면 핸들러에 주석을 달기만 하면 된다.

// cats.controller.ts

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

 

한꺼번에 모으자

이제 RolesGuard 로 다시 돌아가자. 현재는 모든 경우에 단순히 true를 반환하여 모든 요청이 진행될 수 있도록 한다. 현재 사용자에게 할당된 역할과 현재 처리 중인 라우트에 필요한 실제 역할을 비교하여 반환 값을 조건부로 만들고 싶다. 라우트의 역할(사용자 정의 메타데이터)에 액세스하기 위해 다음과 같이 Reflector 헬퍼 클래스를 다시 사용한다.

//roles.guard.ts

import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Roles } from './roles.decorator';

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    const roles = this.reflector.get(Roles, context.getHandler());
    if (!roles) {
      return true;
    }
    const request = context.switchToHttp().getRequest();
    const user = request.user;
    return matchRoles(roles, user.roles);
  }
}

힌트) node.js 세계에서는 인증된 사용자를 request 객체에 연결하는 것이 일반적인 관행이다 . 따라서 위의 샘플 코드에서 request.user 는 사용자 인스턴스와 허용된 역할이 포함되어 있다고 가정한다. 앱에서는 아마도 사용자 정의 인증(authentication) 가드 (또는 미들웨어)에서 해당 연결을 만들 것이다. 이 주제에 대한 자세한 내용은 이 장을 확인하자.

 

주의) matchRoles() 함수 내부의 논리는 필요에 따라 간단하거나 정교할 수 있다. 이 예의 주요 요점은 가드가 요청/응답 주기에 어떻게 부합하는지 보여주는 것이다.

 

컨텍스트 상황에 맞는 방식으로 Reflector를 활용하는 방법에 대한 자세한 내용은 실행 컨텍스트 장의 리플렉션 및 메타데이터 섹션을 참조하자.

권한이 부족한 사용자가 엔드포인트를 요청하면 Nest는 자동으로 다음 응답을 반환한다.

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

백그라운드에서 가드가 false 를 반환하면 프레임워크는 ForbiddenException  를 발생시킵니다 . 다른 오류 응답을 반환하려면 아래와 같이 특정 예외를 발생시켜야 한다. 

throw new UnauthorizedException();

가드에 의해 발생한 모든 예외는 예외 계층 (전역 예외 필터 및 현재 컨텍스트에 적용되는 모든 예외 필터) 에 의해 처리된다.

힌트) 인증 구현 방법에 대한 실제 사례를 찾고 있다면 이 장을 확인하자.

'Backend(Framework) > NestJS 개요(공식문서 번역)' 카테고리의 다른 글

10. Custom decorators  (0) 2023.11.19
9. Interceptors  (0) 2023.11.19
7. Pipes  (0) 2023.11.19
6. Exception filters  (0) 2023.11.17
5. Middleware  (1) 2023.11.16
  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유