https://docs.nestjs.com/guards
세 줄 요약?
@SetMetadata()
로 경로마다 다른 권한 체계 설정 가능?가드는 단일 책임
라우트 핸들러의 요청 처리 여부를 결정한다.
Express에서 미들웨어로 처리하던 것
미들웨어와의 차이점
next()
호출 후)에 어떤 핸들러가 올지 모름ExecutionContext
인스턴스에 접근할 수 있기 때문에 다음에 실행될 것이 뭔지 안다.가드 실행 순서
all middleware → guard → interceptor or pipe
AuthGuard는 요청이 인증된 사용자(요청 헤더에 토큰 첨부됨)라고 가정, 토큰을 추출 및 검증하고 추출된 정보를 이용해서 요청의 처리 여부를 결정한다.
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()
함수는 알아서 구현하삼
이 예제는 가드가 요청/응답 주기에서 어떻게 동작하는지를 보여준다.
모든 가드는 canActivate() 함수를 구현해야 한다. 이 함수는 요청 처리 여부를 boolean으로 리턴한다. (동기 또는 비동기로 리턴 가능)
canActivate()
함수는 ExecutionContext
인스턴스를 인수로 사용한다. ExecutionContext
는 ArgumentsHost
를 extend함 (→ 여러 helper 메서드가 추가됨)
ArgumentsHost
는 Exception filters에서 봤듯이, Request 객체를 얻기 위해 사용한다.
유저가 특정 권한을 가지고 있을 때만 허용하는 가드를 만들어보자.
아래는 가드 기본 템플릿
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;
}
}
파이프나 예외 필터처럼, 가드를 method-scoped, controller-scoped, global-scoped로 만들 수 있다. 지금은 controller-scoped를 만들어 보자.
@Controller('cats')
@UseGuards(RolesGuard)
/**
* 인스턴스를 넘겨줄 수도 있음
* @UseGuards(new RolesGuard())
*/
export class CatsController {}
RolesGuard
인스턴스 대신 타입을 넘겨줌으로써, 프레임워크에게 인스턴스화에 대한 책임을 넘기고 DI를 활성화한다.
글로벌 레벨로 만들고 싶다? Nest 앱의 useGlobalGuards()
메서드를 사용하면 됩니다~
const app = await NestFactory.create(AppModule);
app.useGlobalGuards(new RolesGuard());
하지만 모듈 외부에서 등록되었기 때문에, 의존성 주입이 안됨!
이를 해결하기 위해서는 모듈에서 가드를 설정하면 된다.
import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';
@Module({
providers: [
{
provide: APP_GUARD,
useClass: RolesGuard,
},
],
})
export class AppModule {}
어떤 모듈에 설정하든, 가드는 전역적임
이제 RolesGuard가 작동한다. 그치만.. 모자라
가드의 쩌는 기능 - execution context를 사용해보자.
라우트마다 확인해야 하는 권한이 다르다면 어떻게 해야 할까?
@SetMetadata()
데코레이터를 이용하면 라우트 핸들러에 custom metadata를 붙일 수 있다. (role
데이터 제공)
@Post()
@SetMetadata('roles', ['admin'])
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
create()
메서드에 roles
메타데이터(key: roles, value: [’admin’])를 추가했다.
이것도 작동은 하지만.. @SetMetadata()를 직접 사용하기 보다는, 데코레이터를 만들어서 쓰는 것이 낫다.
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);
}
이제 RolesGuard
와 함께 연결해보자. 현재는 모든 권한에 대해 true를 리턴한다. 이것을 유저 권한과 요청 라우트에 필요한 권한을 비교하여 리턴하는 것으로 바꿔보자. 라우트의 권한(custom metadata)에 접근하기 위해 Reflector
helper class를 이용할 것이다.
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);
}
}
node.js에서는 Request 객체에 유저를 추가하는 것이 일반적이기 때문에, 위 예제에서 request.user
에 유저 인스턴스와 권한이 포함되어 있다고 가정한다.
가드가 false를 반환하는 경우, 프레임워크는 ForbiddenException
을 발생시킨다. 다른 에러 응답을 보내려면, 다른 예외를 던지시오
throw new UnauthorizedException();
가드가 던진 모든 예외는 예외 계층에서 처리된다.