가드는 @Injectable()
데코레이터로 주석이 달린 클래스이다.
가드는 CanActivate
인터페이스를 구현해야 한다.
가드는 단일 책임(Single Responsibility)이 있다.
런타임에 존재하는 특정 조건(ex: 권한, 역할, ACL 등)에 따라 지정된 요청을 route handler에 의해 처리할 지 여부를 결정한다. 이를 Authorization이라고도 한다.
Express에서 Authorization은 middleware에 의해 처리되었다.
하지만 미들웨어로 이를 처리했을 때는 next()
함수를 호출한 후 어떤 handler가 실행될 지 알 수 없다.
반면, 가드는 ExecutionContext
인스턴스에 엑세스할 수 있으므로 다음에 실행될 작업을 정확히 알고 있다.
Authorization은 충분한 권한이 있는 경우에만 특정 route를 사용할 수 있게 해야하므로 가드의 훌륭한 사례라 할 수 있다.
다음의 AuthorGuard
는 이를 구현한 예제이다.(토큰이 요청 헤더에 첨부됐다고 가정한다.)
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);
}
}
모든 가드는 canActivate()
함수를 구현해야 한다.
이 함수는 현재 요청이 허용되는지 여부를 나타내는 bool 값을 반환해야 한다.
응답을 동기식 또는 비동기식으로 반환할 수 있다.(Promise
나 Observable
을 통해)
Nest는 반환값을 사용해 다음 작업을 제어한다.
true
면 요청 처리false
면 Nest가 요청을 거부canActivate()
함수는 ExecutionContext
의 인스턴스를 받고, ExecutionContext
는 ArgumentsHost
에서 상속된다.
위 예에서는 ArgumentsHost
에 정의된 헬퍼 메서드를 이용해 Request
객체에 대한 참조를 얻는다.
ArgumentsHost
를 확장함으로써 ExecutionContext
는 현재 실행 프로세스에 대한 추가 세부정보를 제공하는 몇개의 새로운 헬퍼 메서드를 추가한다.
이러한 세부정보는 광범위한 컨트롤러, 메서드 및 실행 컨텍스트에서 작동할 수 있는 가드를 구축하는데 도움이 된다.
특정 역할을 가진 사용자에게만 엑세스를 허용하는 가드를 만들어보겠다.
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;
}
}
컨트롤러, 메서드, 전역 범위로 가드를 바인딩할 수 있다.
아래 예에서는 @UseGuards()
데코레이터를 사용해 컨트롤러 범위 가드를 설정하였다.
데코레이터 안에는 단일 인수, 쉼표로 구분된 인수 목록을 사용할 수 있다.
@Controller('cats')
@UseGuards(RolesGuard)
export class CatsController {}
인스턴스 대신에 RoleGuard
타입을 전달해 인스턴스 관리를 프레임워크에 맡기고 DI를 적용했다.
마찬가지로 내부 인스턴스를 전달할 수도 있다.
@Controller('cats')
@UseGuards(new RolesGuard())
export class CatsController {}
전역 가드를 설정하기 위해 Nest 어플리케이션 인스턴스의 useGlobalGuards()
메서드를 사용하면 된다.
const app = await NestFactory.create(AppModule);
app.useGlobalGuards(new RolesGuard());
전역 가드는 전체 어플리케이션에서 사용된다.
마찬가지로 DI 관점에서 볼 때, 모듈 외부에서 등록된 전역 가드이기때문에 종속성을 주입할 수 없다.
이를 해결하기 위해서는 모든 모듈에서 직접 가드를 설정해야 한다.
import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';
@Module({
providers: [
{
provide: APP_GUARD,
useClass: RolesGuard,
},
],
})
export class AppModule {}
여태까지의 예에서는 Execution Context를 활용하지 않았다.
역할에 따라 어떤 handler가 허용되는지에 대해서는 아직 알지 못한다.(role
데이터 부재)
예를 들어, CatsController
는 route마다 다른 권한체계를 가질 수 있다. 일부는 관리자만, 일부는 모든 사용자가...
Nest 유연하고 재사용 가능한 방식으로 역할을 route에 매핑시키기 위해 @SetMetadata()
데코레이터를 제공한다.
이는 route handler에 custom metadata를 첨부하는 기능을 제공한다.
이 메타데이터는 가드가 결정을 내리는데 필요한 누락된 role
데이터를 제공한다.
@Post()
@SetMetadata('roles', ['admin'])
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
이렇게 create()
메서드에 roles
메타데이터를 첨부했다.
작동하기는 하지만 이렇게 route에 직접 @SetMetadata()
를 사용하는 것은 지양한다.
아래와 같은 방식을 지향한다.
import { SetMetadata } from '@nestjs/common';
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
이제 custom한 @Roles()
데코레이터가 생성되었으므로, create()
메서드에서 사용할 수 있다.
@Post()
@Roles('admin')
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
이제 이것들을 RoleGuard
와 함께 묶어보도록 하겠다.
현재는 모든 경우에 true
를 반환하므로, 모든 요청이 이루어지게 된다.
현재 사용자의 역할로 route에 접근할 수 있는지 확인 과정을 구현하기 위해 Reflector
헬퍼 클래스를 사용한다.
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);
}
}
통과되지 않은 사용자라면 Nest는 자동으로 다음과 같은 응답을 반환한다.
{
"statusCode": 403,
"message": "Forbidden resource",
"error": "Forbidden"
}
가드가 false
를 반환하면, ForbiddenException
이 발생한다.
다른 오류를 반환하려면 다음과 같이 고유한 예외를 발생시키면 된다.
throw new UnauthorizedException();
가드가 던진 모든 예외는 Exception Layer에 의해 처리된다.