Search
Duplicate

[오버뷰] 08.Guards

Created
2021/07/02 02:54
Tags

GUARD

Guard 클레스는 @Injectable() 데코레이팅 된 클레스이다.
Guard 는 CanActivate 인터페이스를 implement 해야한다.
Guard 는 single responsibility 만 가진다.
Guard 는 주어진 요청이 라우트핸들러에 의해 처리되어야 하는지 아닌지를 결정한다. (현재 런타임의 권한, 규칙, ACLs 등에 따라서)
이것은 authorization 으로 알려져있기도 하다.
Authorization 은 전통적인 express 어플리케이션에서, 일반적으로 미들웨어에 의해 핸들링 되었다.
token validation 과 attaching properties to the request 가 특정 라우트 컨텍스트나 메타데이터 에 강력하게 결합되지 않았기 때문에, 미들웨어는 좋은 선택이였다.
그러나 미들웨어는, 태생적으로 멍청하다.
미들웨어는 next() 함수 이후 어떤 핸들러가 실행 될지 모른다.
반면에 GuardExecutionContext 인스턴스에 접근하기 때문에 다음에 어떠한 라우트 핸들러가 실행될지 알고 있다.
이것은 exception filters, pipes, interceptors 와 비슷하게 설계되었으며, 당신이 request/response cycle 의 정확히 올바른 포인트의 처리 로직에 개입 할 수 있도록 해준다. 그리고 그것을 선언적으로 할수 있게끔 해준다.
이것은 당신의 코드가 좀더 DRY 하고 선언적이게 도와준다.

HINT : Guards 는 모든 미들웨어 다음에 실행되고, 모든 인터셉터와 파이프 이전 에 실행된다. 미들웨어 -> 가드 -> 인터셉터 & 파이프

Authorization guard

말했듯이, authorization 은 guard 의 매우 좋은 사용처 이다, 왜냐하면 특정 라우트는 요청자(일반적으로 권한 인증된 유저) 가 충분한 권한이 있을때 사용 할 수 있어야 하기 때문이다. (말진짜 ㅈㄴ 어렵게하는데 그냥 권한 있는 사람만 특정 라우트에 요청할 수 있어야 된다는 뜻)
우리가 지금 만든 AuthGuard 는 인증된 사용자를 가정하고 만들었다. (그러므로 리퀘스트 헤더에 인증토큰이 붙어있다)
그것은 토큰을 추출하고 validate 할것이다. 그리고 추출된 정보를 요청이 처리될지 안될지를 결정하는데 사용 할 것이다.
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); } }
Plain Text
복사

HINT : 현업에서 어떻게 사용되는지 알고싶으면 this chapter, 마찬가지로 정교한 authorization 의 예는 this page 를 참고하라.

로직 안에 있는 validateRequest() 함수는 필요에 의해 간단하거나, 엄격해질 수 있다.
이 예의 주 목적은 gurad 가 어떻게 요청-응답 사이클에 알맞에 들어가는지 보여주는 것이다.
모든 guard 는 canActivate() 함수를 implement 해야 한다. 이 함수는 반드시 현재 요청이 허락되었는지 아닌지를 가르키는 boolean 을 리턴해야 한다.
비동기, 동기 모두 리턴 할 수 있다. (via a Promise or Observable)
네스트는 다음 액션을 컨트롤하는데 이 리턴 값을 사용한다.
if it returns true, the request will be processed.
if it returns false, Nest will deny the request.

Execution context

canActivate() 함수는 하나의 아규먼트(ExecutionContext)를 가진다. ExecutionContextArgumentsHost를 상속받았다.
우리는 ArgumentsHost 를 이전 exception filter 쳅터에서 이미 봤다.
위의 예에서, 우리는 Request 객체를 참조하기 위해 ArgumentsHost 에 정의되어 있는 우리가 이전에 썼던 같은 헬퍼 메소드를 사용하였다.
이것에 대한 자세한 이야기는 exception filters 쳅터의 Arguments host 섹션을 참고 할 수 있다.
ArgumentHost 의 연장으로, ExecutionContext 는 현재 실행 프로세스 에 대한 디테일한 추가적인 정보를 제공하는 몇몇 새로운 헬퍼 메소드를 가지고 있다.
controllers, methods, execution contexts 을 포함한 모든 것들에 대해 동작하는 좀더 일반적인 guard 를 만드는데 도움을 줄 것이다.
ExecutionContext 에 대해 더 알고싶다면 here

Role-based authentication

유저가 특별한 role 이 있을 때만 ㅈ버근을 허락해주는 좀더 기능적인 가드를 만들어보자.
우리는 기초적인 가드 템플렛에서 시작하여, 이것을 빌드 할 것이다. 지금은, 모든 리퀘스트가 처리되는 것을 허락해준다.
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; } }
Plain Text
복사

Binding guards

pipes 나 exception filters 와 같이, guard can be controller-scoped, method-scoped, or global-scoped.
아래에서, 우리는 컨트롤러 스코프의 가드를 UseGuards() 데코레이터를 사용하여 셋업 하였다.
이 데코레이터는 하나의 아규먼트나 ' ,' 를 이요한 여러 아규먼트를 받을 수 있다.ㅏ
이것은 당신이 한번의 선언에 쉽게 적절한 가드들을 적용시키는게 해준다.
@Controller('cats') @UseGuards(RolesGuard) export class CatsController {}
Plain Text
복사

HINT : The @UseGuards() 데코레이터는 @nestjs/common 패키지로 부터 임포트 되었다.

위에서 우리는 @UseGuards()RolesGuard 타입(인스턴스 대신) 을 제공했다. 프레임워크에 인스턴스화의 책임을 주고, 디펜던시 인젝션을 가능하게 하기 위함이다.
pipe 와 exception filter 에서 처럼, 우리는 그자리에 인스턴스를 줄 수도있다. (아니근데 굳이 내가 만들어줘야할 이유가 없자너 ?)
@Controller('cats') @UseGuards(new RolesGuard()) export class CatsController {}
Plain Text
복사
위 구성은 이 컨트롤러에 해당하는 모든 라우트들에 가드가 붙는 구성이다.
우리가 특정 라우트 메소드에만 가드를 붙이고 싶으면 @UseGuards() 데코레이터를 method level 에 붙이면 된다.
글로벌 가드를 등록하고 싶으면, useGlobalGuards() 를 네스트 어플리케이션 인스턴스 메소드에 사용하면 된다.
const app = await NestFactory.create(AppModule); app.useGlobalGuards(new RolesGuard());
Plain Text
복사
HINT : 하이브리드 앱에서는 정상적으로 동작하지 않는다. 여기 참조
글로벌 가드는 모든 컨트롤러의 모든 라우트 핸들러에 적용된다.
DI 관점에서, 외부 모듈에서 등록된 (useGlobalGuards() 를 사용하여) 글로벌 가드들은 의존성을 주입 할 수 없다. 왜냐면 그것은 외부 모듈에 의해 처리되기 때문이다.
이 이슈를 풀기 위해서 우리는 외부모듈의 아래 구조를 사용해 직접 셋업 해야 한다.
import { Module } from '@nestjs/common'; import { APP_GUARD } from '@nestjs/core'; @Module({ providers: [ { provide: APP_GUARD, useClass: RolesGuard, }, ], }) export class AppModule {}
Plain Text
복사

HINT : 이 접근 방식을 사용하여 가드에 대한 종속성 주입을 수행 할 때, 위 구조가 사용되는 모듈이냐 아니냐에 관계 없이, 가드는 사실상 global 이다. 그럼 어디서 해야 하는가? guard 를 선언한 모듈에서 해라. useClass 만이 커스텀 프로바이더를 등록하는 유일한 방법은 아니다. 여기 를 봐라

Setting roles per handler

우리의 RolesGuard 는 잘 동작하고있다. 하지만 전혀 똑똑하지 않다.
우리는 아직 execution context 라는 가드의 엄청난 이점을 다루지 않았다.
어떤 핸들러에 어떤 롤이 허락되는지 아직 알지 못한다.
CatsController 에서는 예를들어, 다른 라우트 마다 다른 퍼미션이 존재 할 수 있다.
어떤 것은 admin 에게만 허락되었을 수 있고, 어떤것은 모든 사람들에게 허락되었을 수 있다.
어떻게 우리는 롤을 유연하고 재사용성 있는 방법으로 라우트에 적용 시킬 수 있을 까?
이곳이 바로 커스텀 메타데이터가 작동하는 곳이다. (learn more here
네스트는 @SetMetadata 데코레이터를 통해 라우트에 커스텀 메타데이터를 붙이는 것을 제공한다.
메타데이터는 우리에게 빠진 role 데이터 (스마트 가드가 판단하는데 필요한) 를 제공해준다.
roles 라는 메타데이터를 라우트에 집어넣고, 가드는 미들웨어와 다르게 이후 처리되는 라우트 핸들러의 메타데이터에 접근 할 수 있기 때문에, 라우트메타데이터에 접근하여 roles 라는 메타데이터의 값을 가져와서, 실제 들어온 request 의 roles 와 라우트 메타데이터의 roles 가 맞는지 확인하는 식으로 사용하라는듯?
아무튼 아래와 같이 사용한다.
@Post() @SetMetadata('roles', ['admin']) async create(@Body() createCatDto: CreateCatDto) { this.catsService.create(createCatDto); }
Plain Text
복사

HINT : The @SetMetadata() 데코레이터는 @nestjs/common 으로부터 임포트된다.

위처럼 있는 (key 가 roles 이고, value 가 ['admin'] 인) roles 메타데이터를 붙이는건 별로 좋지 않은 행동이다.
대신에 당신만의 데코레이터를 만들어 붙여야 한다.
아래와 같이
import { SetMetadata } from '@nestjs/common'; export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
Plain Text
복사
이것이 좀더 깔끔하고 읽기 쉽다. 그리고 강력하게 타입하다.
우리는 이제 아래와 같이 바꿀 수 있다.
@Post() @Roles('admin') async create(@Body() createCatDto: CreateCatDto) { this.catsService.create(createCatDto); }
Plain Text
복사

Putting it all together

우리가 가드에서 라우트에 있는 메타데이터에 접근할때 Reflector 헬퍼 클레스를 사용한다.
@nestjs/core 패키지에서 제공한다.
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); } }
Plain Text
복사