처음부터 차근차근

[NestJS] Guard 본문

FrameWork/NestJS

[NestJS] Guard

HangJu_95 2023. 11. 27. 22:46
728x90

Guard란?

  • Guard는 특정 경로로의 요청을 승인하는지에 대한 판단을 내리는 역할을 맡습니다.
  • 주로 인증과 인가에 사용됩니다.
  • Guard는 ExecutionContext(실행 컨텍스트)를 알고 있으며, Express와는 다르게 ExecutionContext를 통하여 request와 response를 받기 때문에 Middleware보다 더 똑똑합니다.
Guards have a single responsibility. They determine whether a given request will be handled by the route handler or not, depending on certain conditions (like permissions, roles, ACLs, etc.)
- NestJS 공식 문서

즉, 허용된 유저가 아니면 요청 자체를 막아버리는 것입니다. 모든 사용자가 서버에 요청을 단시간에 많이 할 수 있다면, DDos같은 엄청난 트래픽이 들어올 때 모든 요청에 대하여 응답을 하게 될 것 입니다. 이러한 상황을 막기 위해 Throttler 모듈을 이용해 Rate limit을 제한하는 방식도 Guard를 통해서 가능합니다.

Authorization guard

먼저 NestJS 공식문서에 있는 예시를 통해 설명해보겠습니다.

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

@Injectable()
// AuthGuard 클래스를 만들면서, CanActivate 인터페이스를 상속받습니다.
export class AuthGuard implements CanActivate {
  // canActivate는 True, false값을 반환해야 합니다.
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
  	ExecutionContext를 통해 Request 정보를 받아옵니다.
    const request = context.switchToHttp().getRequest();
    // validateRequest 함수를 통해 요청을 허가할지 판단하고, 허가하면 true, 아니면 false 반환
    // 이 함수는 로그인 인증 로직이 담겨있을 수 있고, 유저 판단 정보가 담겨있을 수 있다.
    return validateRequest(request);
  }
}

모든 Guard 파일은 CanActivate 인터페이스를 상속받아야 하고, canActivate 함수를 사용하여 인증 로직을 작성해야 합니다.

canActivate 함수는  boolean 값을 return 하며, 이를 통해 해당하는 요청을 수행할지 거부할 지 판단합니다.

  • if it returns true, the request will be processed.
  • if it returns false, Nest will deny the request.

Execution context

canActivate 함수에는 하나의 인자를 갖는데, 그것은 ExecutionContext(실행컨텍스트)입니다. 

실행컨텍스트에는 argumentsHost와 argumentHandler를 가지고 있는데, 이는 실행컨텍스트에서 따로 설명하겠습니다.

간단하게 설명하자면, 실행할 코드에 제공할 환경 정보들을 모아놓은 객체라고 생각하면 쉬우며, 이것을 통해 Request 객체 혹은 Response 객체를 받을 수 있습니다.

Role-based authentication

또 다른 예시를 통해 Guard를 한번 만들어보겠습니다.

이 Guard는 Role을 통해 사용자에게 권한을 인가할 수 있으며, 현재는 모든 사용자가 요청할 수 있도록 만들었습니다.

//role.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;
  }
}

현재는 모든 canActivate 값이 true를 반환하기 때문에, 이 Guard를 사용한다면 모든 요청이 허가됩니다.

Binding guards

이제는 만들어본 Guard를 controller에 적용시켜보겠습니다.

Guard는 전역, 컨트롤러, 라우터 단위 스코프로 적용할 수 있고, UseGuards() 데코레이터를 이용하여 Guard를 적용할 수 있습니다.

@Controller('cats')
// UseGuards안에 적용시킬 가드를 매개변수로 작성합니다.
@UseGuards(RolesGuard)
export class CatsController {}

위 예시는 RolesGuard 클래스를 전달해 인스턴스화에 대한 책임을 프레임워크에 맡기고 종속성 주입을 활성화했지만, Pipe나 exception filter처럼 마찬가지로 내부 인스턴스를 전달할 수 있습니다.

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

만약, 전역으로 Guard를 사용하고 싶다면 Nest Application 인스턴스의 메소드를 사용해서 적용시킬 수 있습니다.

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

현재 적용되어 있는 전역 가드는 모듈 외부에서 등록된 가드이므로 종속성을 주입할 수 없습니다. 모듈 컨텍스트 외부에서 수행되기 때문이며, 이를 해결하기 위해서는 root 모듈에 직접 가드를 설정할 수 있습니다.

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

하지만 아직 RolesGuard는 true값만 return하고 있습니다. 어떠한 실행컨텍스트에서도 인자를 받지 않습니다.

우리는 customMetadeta를  추가하여 유저의 권한을 확인할 수 있도록 만들겁니다.

 

Nest에서는 내장된 데코레이터를 통해 route handler에 customMetadata를 추가하는 기능을 가지고 있습니다. 

간단한 예시를 통해 하나 만들어보겠습니다.

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

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

Reflector.createDecorator라는 메소드를 통해 @Roles라는 데코레이터를 만들었습니다. 

이 메서드를 통해서 해당 Router에 통과할 수 있는 사용자 권한 Level을 받아올 겁니다.

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

@Roles 데코레이터를 추가하여 해당하는 Roles의 Level이 admin이라고 추가하였습니다.

즉, admin이 아니면 해당하는 router에 접근할 수 없게 하는거죠.

이제 rolesguard를 변경해보겠습니다.

(metadata와 Reflector는 Execution context에서 따로 다룰 예정입니다.)

Putting it all together

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 {
    // reflector.get을 통해 roles 데코레이터 요청하는 route에 적용되어있는지 확인
    const roles = this.reflector.get(Roles, context.getHandler());
    if (!roles) {
    // 적용되어 있지 않다면, Guard를 바로 통과시키도록 한다.
      return true;
    }
    // 사용자의 정보를 확인
    const request = context.switchToHttp().getRequest();
    const user = request.user;
    // matchRoles 메서드를 통해 roles의 권한과 user의 권한이 동일한지 확인한다.
    return matchRoles(roles, user.roles);
  }
}

이제 RolesGuard를 변경해보았습니다.

Roles가 적용되어있지 않다면 바로 통과시키고, 만약 Roles가 적용되어 있다면 해당하는 요청의 user가 그것에 맞는 권한이 있는지 확인하는 과정을 거쳐야합니다.

 

이렇게 NestJS의 예시를 통해 Guard를 알아봤습니다.

Guard는 다양한 인증과 인가를 통해서 사용될 수 있으며, HTTP request, response에만 적용되어있는 것이 아닌, 실행컨텍스트를 통해 GraphQL, gRPC 등 다양한 방식으로도 인증 인가를 진행할 수 있습니다.

참고

https://docs.nestjs.com/guards

https://gongmeda.tistory.com/56?category=1043173

https://velog.io/@junguksim/NestJS-%EB%85%B8%ED%8A%B8-2-Guards

 

'FrameWork > NestJS' 카테고리의 다른 글

[NestJS] Pipe  (0) 2023.11.28
[NestJS] interceptor  (0) 2023.11.27
[NestJS] Middleware  (0) 2023.11.25
[NestJS] Request Lifecycle  (0) 2023.11.25
[NestJS] Module  (1) 2023.11.24