[NestJS 공식문서 정독하기] Overview - Guards

2022. 8. 2. 17:31Web/NestJS

반응형
🔗  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 Progamming), FP (Functional Programming), and FRP (Functional Reac

docs.nestjs.com

  • guard는 CanActivate 인터페이스를 구현하며 @Injectable() 데코레이터가 적용되어 있는 클래스임
  • guard는 여러가지 조건들에 따라 주어진 요청이 route handler에 의해 처리될지 여부를 판단하는 역할임 (authorization & authentication)
  • 기존에는 보편적으로 middleware가 해당 처리를 했지만 middleware는 멍청함 (어떤 handler에 의해 처리될지 모름)
  • 그에 비해 gaurd는 ExecutionContext 를 알고 exception filter, interceptor와 유사한 구조를 가짐
  • guard는 미들웨어와 interceptor/pipe 사이에 실행됨

Authorization guard

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(request) 는 인증 로직이 포함된 함수임 (생략)
  • 모든 guard는 canActivate 메소드를 구현해야하며 해당 메소드는 해당 요청의 유효 여부인 boolean을 반환함

Execution context

  • ExecutionContextArgumentsHost 상속 받음
  • 위 예제에서는Request 객체에 접근하기 위해 사용함
  • 여기에서 ExecutionContext 에 대해 더 자세히 알 수 있음

Role-based authentication

Binding guards

@Controller('cats')
@UseGuards(RolesGuard)
export class CatsController {}
@Controller('cats')
@UseGuards(new RolesGuard())
export class CatsController {}
const app = await NestFactory.create(AppModule);
app.useGlobalGuards(new RolesGuard());
  • guard는 controller-scoped, method-scoped, global-scoped 모두 가능
  • @UseGuards() 데코레이터나 useGlobalGuards 메소드로 원하는 스코프에 맞게 적용할 수 있음
import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';

@Module({
  providers: [
    {
      provide: APP_GUARD,
      useClass: RolesGuard,
    },
  ],
})
export class AppModule {}
  • pipe와 동일하게 DI를 활용하기 위해서 모듈을 통해 설정할 수 있음
  • pipe와 동일하게 이렇게 설정할 경우 모듈과 무관하게 모두 global 스코프가 되며 guard가 정의된 모듈에서 설정하는 것을 권장함

Setting roles per handler

@Post()
@SetMetadata('roles', ['admin'])
async create(@Body() createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);
}
  • 유연하고 재사용 가능한 방법으로 role을 route에 매칭하기 위해 custom metadata를 활용할 수 있음
  • NestJS는 @SetMetadata() 를 활용해 handler에 custom metadata를 부여할 수 있는 기능을 제공함
  • 위 예제에서 handler에 role이라는 metadata를 부여함
import { SetMetadata } from '@nestjs/common';

export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
@Post()
@Roles('admin')
async create(@Body() createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);
}
  • 다만, @SetMetadata() 를 직접 사용하는 것은 좋은 방식이 아님
  • 그래서 위와 같이 자체 데코레이터를 만들어서 사용하는 것이 좋음
  • 해당 방식이 훨씬 깔끔하고 가독성이 좋으며 strongly typed임

Putting it all together

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);
  }
}
  • route의 custom metadata에 접근하기 위해 Reflector helper 클래스를 활용함 (NestJS 내장)
  • 위 예제에서는 request.user 가 user 인스턴스와 role들을 포함한다고 가정
  • matchRoles(roles, user.roles) 의 로직은 user의 권한과 handler의 권한을 비교하는 로직임 (생략)
  • guard에서 false를 반환할 경우, 프레임워크는 자동으로 ForbiddenException 을 던짐 (다른 예외를 던지고 싶으면 다른 예외를 직접 던지면 됨)
  • guard에서 던지는 예외는 pipe와 마찬가지로 exception layer에서 처리하므로 exception filter가 처리함
반응형