
guard는 @Injecable()) 데코레이터와 annotated되는 클래스이다. CanActivate interface를 반드시 implement해야한다.

Guard는 단일 책임을 가진다. 특정 상황들(permissions, roles, ACLs..)에 따라서 주어진 request가 route handler에 의해 handle될지 말지를 결정한다. 이를 authorization이라고 한다.
Authorization은 전통적인 Express 애플리케이션에서는 주로 미들웨어로 handle 되었다. 미들웨어는 인증에 좋은 선택이다. 토큰 유효성 검사나 요청 객체에 프로퍼티 첨부 같은 것들은 특정 route의 context에 크게 연관이 없기 때문이다.
하지만 미들웨어는 멍청하다(?). 미들웨어는 next()가 call된 후 어떤 handler가 실행될지를 전혀 모른다. 하지만 Guard는 ExcutionContext 인스턴스를 사용할 수 있고, 다음에 어떤 것들이 실행될 지 정확하게 알 수 있다. Guards는 exception filter, pipe, interceptor와 비슷하게 디자인 되었다. 처리 로직을 request/response의 cycle 내 정확한 곳에 삽입할 수 있도록 한다. 이는 코드를 DRY하고 declarative(선언적)하게 유지하는데 도움이 된다.
Guards는 모든 middleware 다음에 실행되고, interceptor나 pipe 이전에 실행된다.
authorization은 특정 route들이 충분한 permission이 주어지고 난 후에만 가능하기 때문에 guard의 좋은 케이스이다. AuthGuard는 토큰을 검증하고, 추출한 정보로부터 해당 request가 실행될 수 있는지 결정할 것이다.
import { Injectable, CanActive, ExcutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';
@Injectable()
export class AuthGuard implements CanActivate {
canActivate(
context: ExcutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
const request = context.switchToHttp().getRequest();
return validateRequest(request);
}
}
(실제 사례는 Authorization 문서에 있다고 한다.)
모든 guard는 canActivate() 함수를 implement해야한다. 이 함수는 현재의 request가 실행될 수 있는지 없는지 나타내는 boolean을 리턴해야한다.
canActivate()는 하나의 ExcutionContext 인스턴스 arg를 가진다. ExcutionContext는 ArgumentsHost로부터 상속된다. Request object를 참조하기 위해서 ArgumentHost에 정의된 helper method들과 같은 것들을 사용하고 있다. (Exception Filter 문서로 가보면 관련 설명을 볼 수 있다)
AtgumentHost, ExecutionContext를 extend하면서 현재 실행되는 프로세스의 다양한 정보를 가져올 수 있는 새로운 helper method들을 가진다.
특정 role을 가지고 있는 유저만 허락하는 guard를 만들어보자.
import { Injectable, CanActivate, ExcutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';
@Injectable()
export class RolesGuard implements CanActivate {
canActivate(
context: ExcutionContext
): boolean | Promise<boolean> | Observable<boolean> {
return true;
}
}
Pipe나 filter처럼, guard도 @UseGuards() 데코레이터로controller-scoped, method-scoped, global-scoped가 될 수 있다. 이 데코레이터는 하나 혹은 여러 개의 콤마로 구분된 argument를 가진다.
@Controller('cats')
@UseGuards(RoleGuard)
export class CatsController {}
@UseGuards()데코레이터는@nestjs/common패키지 안에 있다.
위에서 인스턴스 대신 RoleGuard를 전달해서 인스턴스화에 대한 책임을 프레임워크에 맡기고 종속성 주입을 가능하게 했다.
@Controller('cats')
@UseGuards(new RoleGuard())
export class CatsController {}
이런 식으로도 쓸 수 있다. 이러면 이 controller 내의 모든 handler에 guard가 붙는다. 하나의 메소드에만 적용하고 싶다면 해당 메소드에 @UseGuards()를 붙여주면 된다.
global guard를 설정하려면, @UseGlobalGuards()를 사용하면 된다.
const app = awiat NestFactory.create(AppModule);
app.useGlobalGuards(new RolesGuard());
Hybird Apps 내에서,
useGlobalGuards()는 gateways와 micro services에게 guard를 설정해주지 않는다(?)
종속성 주입 관점에서 모듈 외부에 등록된 글로벌 가드는 종속성을 주입할 수 없다.(위처럼 useGlobalGuards() 사용) 해당 문제를 해결하기 위해서는 module에 gurad를 설정해주는게 좋다.
import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';
@Module({
providers: [
{
provider: APP_GUARD,
useClass: RolesGuard,
},
]
})
export class AppModule {}
아직 우리는 guard의 가장 큰 특징인 Excution Context에서 얻을 수 있는 이득을 보고 있지 않다. 아직 roles에 관해서는 모르는 상태고, 어떤 role이 handler에게 허용되는지도 모른다. CatsController에서 어떤 routes는 admin에게만 사용 가능해야하고, 어떤 routes는 모두 사용이 가능해야한다. 어떻게 flexible하고 reusable하게 정해줄 수 있을까.
여기서 custom metadata가 등장한다. Nest는 SetMetadata() 데코레이터를 통해 route handler에 custom metadata를 붙일 수 있게 해준다. 이 metadata는 smart guard가 결정을 내리기 위해 필요한 누락된 role의 데이터를 보충해준다.
@Post()
@SetMetadata('roles', ['admin'])
async create(@Body() createDto: CreateDto) {
this.catsService.create(createDto);
}
위의 예시는 작동하긴 하지만 routes에 직접 SetMetadata()를 사용하는 것은 좋지 않다. 대신 직접 decorator를 만드는 것이 좋다.
import { SetMetadata } from '@nestjs/common';
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
@Post()
@Roles('admin')
async create(@Body() createDto: CreateDto) {
this.catsService.create(createDto);
}
이게 좋은 방식이다.
이제 다시 RolesGuard로 가서 묶어보자. 현재는 모든 case에 대해 true를 return하도록 되어있기에 모든 request가 진행될 것이다. 우리는 현재 user에게 배정된 role을, 현재 route가 실제로 요구하는 role과 비교함으로써, RoleGuard가 return하는 값을 알맞게 설정할 것이다. route의 role에 접근하기 위해 우리는 Reflector라는 helper class를 사용할 것이다.
import { Injectable, CanActivate, ExcutionContext } 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.rolse);
}
}
코드 이해 불가...
만약 권한이 부족한 사용자가 엔드포인드를 요청하면 Nest는 자동적으로 아래를 리턴할 것이다.
{
"statusCode": 403,
"message": "Forbidden resource",
"error": "Forbidden"
}
guard가 false를 return할 때 framework가 ForbiddenException을 throw 한다. 만약 다른 response를 반환하고 싶다면 특정 exception을 throw 하도록 해야 한다. guard에 thrown된 모든 exception은 exceptions layer에서 handle 될 수 있다.