GUARD
•
Guard 클레스는 @Injectable() 데코레이팅 된 클레스이다.
•
Guard 는 CanActivate 인터페이스를 implement 해야한다.
•
Guard 는 single responsibility 만 가진다.
•
Guard 는 주어진 요청이 라우트핸들러에 의해 처리되어야 하는지 아닌지를 결정한다. (현재 런타임의 권한, 규칙, ACLs 등에 따라서)
•
이것은 authorization 으로 알려져있기도 하다.
•
Authorization 은 전통적인 express 어플리케이션에서, 일반적으로 미들웨어에 의해 핸들링 되었다.
•
token validation 과 attaching properties to the request 가 특정 라우트 컨텍스트나 메타데이터 에 강력하게 결합되지 않았기 때문에, 미들웨어는 좋은 선택이였다.
•
그러나 미들웨어는, 태생적으로 멍청하다.
•
미들웨어는 next() 함수 이후 어떤 핸들러가 실행 될지 모른다.
•
반면에 Guard 는 ExecutionContext 인스턴스에 접근하기 때문에 다음에 어떠한 라우트 핸들러가 실행될지 알고 있다.
•
이것은 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)를 가진다. ExecutionContext 는 ArgumentsHost를 상속받았다.
•
우리는 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
복사
•
글로벌 가드는 모든 컨트롤러의 모든 라우트 핸들러에 적용된다.
•
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 는 잘 동작하고있다. 하지만 전혀 똑똑하지 않다.
•
•
어떤 핸들러에 어떤 롤이 허락되는지 아직 알지 못한다.
•
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
복사