Guards

이연중·2021년 7월 28일
0

NestJS

목록 보기
8/22
post-custom-banner

가드는 @Injectable() 데코레이터로 주석이 달린 클래스이다.

가드는 CanActivate 인터페이스를 구현해야 한다.

가드는 단일 책임(Single Responsibility)이 있다.

런타임에 존재하는 특정 조건(ex: 권한, 역할, ACL 등)에 따라 지정된 요청을 route handler에 의해 처리할 지 여부를 결정한다. 이를 Authorization이라고도 한다.

Express에서 Authorization은 middleware에 의해 처리되었다.

하지만 미들웨어로 이를 처리했을 때는 next() 함수를 호출한 후 어떤 handler가 실행될 지 알 수 없다.

반면, 가드는 ExecutionContext 인스턴스에 엑세스할 수 있으므로 다음에 실행될 작업을 정확히 알고 있다.

Authorization Guard


Authorization은 충분한 권한이 있는 경우에만 특정 route를 사용할 수 있게 해야하므로 가드의 훌륭한 사례라 할 수 있다.

다음의 AuthorGuard는 이를 구현한 예제이다.(토큰이 요청 헤더에 첨부됐다고 가정한다.)

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

모든 가드는 canActivate() 함수를 구현해야 한다.

이 함수는 현재 요청이 허용되는지 여부를 나타내는 bool 값을 반환해야 한다.

응답을 동기식 또는 비동기식으로 반환할 수 있다.(PromiseObservable을 통해)

Nest는 반환값을 사용해 다음 작업을 제어한다.

  • true면 요청 처리
  • false면 Nest가 요청을 거부

Execution Context


canActivate() 함수는 ExecutionContext의 인스턴스를 받고, ExecutionContextArgumentsHost에서 상속된다.

위 예에서는 ArgumentsHost에 정의된 헬퍼 메서드를 이용해 Request 객체에 대한 참조를 얻는다.

ArgumentsHost를 확장함으로써 ExecutionContext는 현재 실행 프로세스에 대한 추가 세부정보를 제공하는 몇개의 새로운 헬퍼 메서드를 추가한다.

이러한 세부정보는 광범위한 컨트롤러, 메서드 및 실행 컨텍스트에서 작동할 수 있는 가드를 구축하는데 도움이 된다.

Role-based Authentication


특정 역할을 가진 사용자에게만 엑세스를 허용하는 가드를 만들어보겠다.

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

Binding Guards


컨트롤러, 메서드, 전역 범위로 가드를 바인딩할 수 있다.

아래 예에서는 @UseGuards() 데코레이터를 사용해 컨트롤러 범위 가드를 설정하였다.

데코레이터 안에는 단일 인수, 쉼표로 구분된 인수 목록을 사용할 수 있다.

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

인스턴스 대신에 RoleGuard 타입을 전달해 인스턴스 관리를 프레임워크에 맡기고 DI를 적용했다.

마찬가지로 내부 인스턴스를 전달할 수도 있다.

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

전역 가드를 설정하기 위해 Nest 어플리케이션 인스턴스의 useGlobalGuards() 메서드를 사용하면 된다.

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

전역 가드는 전체 어플리케이션에서 사용된다.

마찬가지로 DI 관점에서 볼 때, 모듈 외부에서 등록된 전역 가드이기때문에 종속성을 주입할 수 없다.

이를 해결하기 위해서는 모든 모듈에서 직접 가드를 설정해야 한다.

import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';

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

Setting Roles Per Handler


여태까지의 예에서는 Execution Context를 활용하지 않았다.

역할에 따라 어떤 handler가 허용되는지에 대해서는 아직 알지 못한다.(role 데이터 부재)

예를 들어, CatsController는 route마다 다른 권한체계를 가질 수 있다. 일부는 관리자만, 일부는 모든 사용자가...

Nest 유연하고 재사용 가능한 방식으로 역할을 route에 매핑시키기 위해 @SetMetadata() 데코레이터를 제공한다.

이는 route handler에 custom metadata를 첨부하는 기능을 제공한다.

이 메타데이터는 가드가 결정을 내리는데 필요한 누락된 role 데이터를 제공한다.

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

이렇게 create() 메서드에 roles 메타데이터를 첨부했다.

작동하기는 하지만 이렇게 route에 직접 @SetMetadata()를 사용하는 것은 지양한다.

아래와 같은 방식을 지향한다.

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

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

이제 custom한 @Roles() 데코레이터가 생성되었으므로, create() 메서드에서 사용할 수 있다.

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

Putting It All Together


이제 이것들을 RoleGuard와 함께 묶어보도록 하겠다.

현재는 모든 경우에 true를 반환하므로, 모든 요청이 이루어지게 된다.

현재 사용자의 역할로 route에 접근할 수 있는지 확인 과정을 구현하기 위해 Reflector 헬퍼 클래스를 사용한다.

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();

가드가 던진 모든 예외는 Exception Layer에 의해 처리된다.

참고

https://docs.nestjs.kr/guards

profile
Always's Archives
post-custom-banner

0개의 댓글