Skip to content

8. Guards


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


001


가드는 런타임에 존재하는 특정 조건(권한, 역할 등)에 따라 주어진 요청이 라우트 핸들러에 의해 처리될지 여부를 결정한다. 이것을 흔히 인가/인증(authorization/authentication)이라고 한다. 인가/인증은 일반적으로 기존 Express 애플리케이션의 미들웨어에 의해 처리되었다. 미들웨어는 인증을 위한 좋은 선택이다. 토큰 유효성 검사 및 요청 객체에 프로퍼티 연결과 같은 작업이 특정 라우트 컨텍스트(및 해당 메타데이터)와 강력하게 연결되어 있지 않기 때문이다.


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


Note

가드는 각 미들웨어 이후에 실행되지만 인터셉터나 파이프 이전에 실행된다.


1. Authorization guard

인가는 호출자(인증된 특정 사용자)에게 충분한 권한이 있는 경우에만 특정 경로를 사용할 수 있게 한다. 아래의 예제에서 빌드할 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는 요청을 거부한다.


2. Execution context

canActivate() 함수는 단일 인수인 ExecutionContext 인스턴스를 사용한다. ExexcutionContextArgumentsHost에서 상속된다. 이전의 예외 필터 예제에서 ArgumentsHost를 살펴봤다. 위의 샘플에서는 이전에 사용했던 ArgumentsHost에 정의된 동일한 헬퍼 메서드를 사용하여 Request 객체에 대한 참조를 가져온다.


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


3. Role-based authentication

특정 역할을 가진 사용자에게만 접근을 허용하는 보다 기능적인 가드를 만들어 보자.


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;
  }
}


4. Binding guards

파이프 및 예외 필터와 마찬가지로 가드는 컨트롤러 범위, 메서드 범위 또는 글로벌 범위일 수 있다. 아래에서 @UseGuards() 데코레이터를 사용하여 컨트롤러 범위 가드를 설정한다. 이 데코레이터는 단일 인수 또는 쉼표로 구분된 인수 목록을 사용할 수 있다. 이를 통해 하나의 선언으로 적절한 가드 세트를 쉽게 적용할 수 있다.


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


위에서 인스턴스 대신 RolesGuard 타입을 전달하여 프레임워크에 인스턴스화에 대한 책임을 맡기고 종속성 주입을 활성화했다. 파이프 및 예외 필터와 마찬가지로 내부 인스턴스를 전달할 수도 있다.


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


위의 구성은 이 컨트롤러가 선언한 모든 핸들러에 가드를 연결한다. 가드가 단일 메서드에만 적용되도록 하려면 메서드 레벨에서 @UseGuards() 데코레이터를 적용한다.


글로벌 가드를 설정하려면 Nest 애플리케이션 인스턴스의 useGlobalGuards() 메서드를 사용한다.


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


글로벌 가드는 모든 컨트롤러와 모든 라우트 핸들러에 대해 전체 애플리케이션에서 사용된다. 종속성 주입의 관점에서, 모듈 외부에서 등록된 글로벌 가드는 종속성을 주입할 수 없다. 이는 모든 모듈의 컨텍스트 외부에서 수행되기 때문이다. 이 문제를 해결하기 위해 다음 구성을 사용하여 모든 모듈에서 직접 가드를 설정할 수 있다.


app.module.ts
import { Module } from "@nestjs/common";
import { APP_GUARD } from "@nestjs/core";

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


5. Setting roles per handler

RolesGuard가 작동하고 있지만 가장 중요한 보호 기능인 실행 컨텍스트를 활용하지 않았기 때문에 아직 부족하다. 아직 역할이나 각 핸들러에 허용되는 역할에 대해 알지 못한다. 예를 들어 CatsController는 다른 라우트에 대해 다른 권한 체계를 가질 수 있다. 일부는 관리자만 사용할 수 있고 다른 일부는 모든 사람에게 공개될 수 있다.


여기서 사용자 정의 메타데이터가 작동한다. Nest는 @SetMetadata() 데코레이터를 통해 라우트 핸들러에 사용자 정의 메타데이터를 첨부하는 기능을 제공한다. 이 메타데이터는 스마트 가드가 결정을 내리는 데 필요한 누락된 역할 데이터를 제공한다. @SetMetadata()를 사용하는 방법을 살펴보자.


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


위의 구성을 통해 역할 메타데이터(roles는 키이고 ['admin']은 특정 값임)를 create() 메서드에 첨부했다. 이것이 작동하는 동안 라우트에서 직접 @SetMetadata()를 사용하는 것은 좋은 습관이 아니다. 대신 아래와 같이 자신만의 데코레이터를 만든다.


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

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


이 접근 방식은 훨씬 깨끗하고 읽기 쉬우며 강력한 형식이다. 이제 사용자 정의 @Roles() 데코레이터가 있으므로 이를 사용하여 create() 메서드를 장식할 수 있다.


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


6. Putting it all together

이제 돌아가서 이것을 RolesGuard와 연결해 보자. 현재는 모든 경우에 단순히 true를 반환하여 모든 요청을 계속 진행할 수 있다. 현재 사용자에게 할당된 역할을 처리 중인 현재 라우트에 필요한 실제 역할과 비교하여 반환 값을 조건부로 만든다. 라우트의 역할(사용자 정의 메타데이터)에 액세스하기 위해 프레임워크에서 기본 제공되고 @nestjs/core 패키지에서 노출되는 Reflector 헬퍼 클래스를 사용한다.


roles.guard.ts
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());
    if (!roles) {
      return true;
    }
    const request = context.switchToHttp().getRequest();
    const user = request.user;
    return matchRoles(roles, user.roles);
  }
}


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


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


가드가 false를 반환하면 프레임워크가 ForbiddenException을 던진다. 다른 오류 응답을 반환하려면 고유한 예외를 던져야 한다.


throw new UnauthorizedException();


가드가 던진 모든 예외는 예외 레이어에서 처리된다.


References