NestJs

NestJs - Guard

hs-archive 2023. 3. 9. 22:49

Guard ( 가드 )

nestjs에서 Guard란 "CanActivate" 인터페이스를 구현하는 "@Injectable()" 데코레이터로 주석이 달린 클래스를 뜻합니다. 쉽게 말해, 아래처럼 생긴 클래스가 있다면 무조건 가드라고 부르는 것입니다.

 

@Injectable()
class SomeGuard implements CanActivate {
	canActivate(context: ExecutionContext) {
		...
	}
}

 

Guard가 하는 일은 런타임에 존재하는 특정 조건(예: 권한, 역할, ACL 등)에 따라 요청을 이다음으로 넘길지 말지를 결정하는 것입니다. canActive() 함수에서 true를 반환하면 요청이 다음 단계로 넘어가고 false를 반환하면 Nest는 요청을 거부합니다. canActive() 함수는 return을 동기식 또는 비동기식으로 할 수 있습니다.

 

더보기
request lifecycle in nestjs

 

미들웨어 이후에 가드가 실행되고 가드 이후에 인터셉터, 파이프가 실행됩니다.

 

가드 만들고 사용해보기

"authorization"은 호출자(일반적으로 인증(authenticated)된 특정 사용자)에게 충분한 "권한"이 있는지 확인하는 것을 의미합니다. 인증이 정상적으로 완료되면 요청을 "허가"하고 그렇지 않으면 "거부"합니다. 따라서 이러한 상황에서 Guard를 사용하는 것은 가드의 훌륭한 사용 사례입니다.

한 번 가드를 사용하여 "authorization"을 달성해봅시다.

 

우리가 구축할 AuthGuard는 인증된 사용자(authenticeted user)를 가정하므로 요청 헤더에 토큰이 첨부되었다고 생각합시다. 토큰을 추출 및 검증하고, 추출된 정보를 사용하여 요청을 진행할지 말지 여부를 결정하는 가드는 아래와 같이 작성할 수 있습니다.

 

<auth.guard.ts>

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();
        
		// requset 헤더에 담긴 토큰을 추출
		// 해당 토큰을 검증
		// 토큰에 담긴 정보로부터 요청이 적절한 권한을 가졌는지 확인 ...
		// return boolean
	}
}

 

주석 처리된 canActivate() 함수 내부 로직은 구현에 따라 간단하거나 복잡할 수 있습니다. 이 예제의 요점은 가드가 canActivate()의 context 매개변수를 사용하여 사용자가 보낸 request에 접근할 수 있고, 따라서 request에 담긴 여러 값(이 경우 헤더에 담긴 Token)을 가져와 애플리케이션이 요구하는 인증을 처리할 수 있다는 것입니다. 필요하다면 context를 통해 response도 끄집어낼 수 있습니다. ExecutionContext는 많은 것을 제공해 줍니다.

 

이렇게 만든 가드는 컨트롤러 범위, 메서드 범위, 전역 범위에 설정할 수 있습니다. 아래에서 @UserGards() 데코레이터를 사용하여 컨트롤러 범위 가드를 설정합니다. 이 @UserGards()는 단일 인수 또는 쉼표로 구분된 인수 목록을 받습니다. 아래와 같이 설정하면 해당 CatsController 안에 있는 모든 메서드들에 대해 AuthGard가 적용됩니다.

 

<cats.controller.ts>

@Controller('cats')
@UseGuards(AuthGuard)
export class CatsController {
	// ... Some Method
}

 

만약 AuthGard를 단일 Method 단위로 적용하고 싶다면 해당되는 메서드 위에 @UseGuards(AuthGuard)를 적으면 됩니다.

 

<cats.controller.ts>

@Controller('cats')
export class CatsController {

	@UseGuards(AuthGuard)
	async a_method() {
            // ...
        }
    
}

 

만약 전역 범위로 가드를 설정하고 싶다면 Nest 애플리케이션 인스턴스의 useGlobalUards() 메서드를 사용하면 됩니다.

 

const app = await NestFactory.create(AppModule);
app.useGlobalGuards(new AuthGuard());

 

단, 종속성 주입 측면에서 모듈 외부에서 등록된 글로벌 가드(위의 예에서와 같이 useGlobalGuards() 사용)는 종속성을 주입할 수 없습니다. 이는 모듈 콘텍스트 외부에서 수행되기 때문입니다. 이 문제를 해결하기 위해 다음 구성을 사용하여 모든 모듈에서 직접 가드를 설정할 수 있습니다.

 

<app.module.ts>

import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';

@Module({
	providers: [{
		provide: APP_GUARD,
		useClass: AuthGuard,
	}],
})
export class AppModule {}

 

이번에는, 우리가 게시판 애플리케이션을 만들고 있다고 생각해 봅시다. 글쓰기는 "admin"과 "user" 모두 가능하고 차단 기능은 "admin"만 할 수 있다고 합시다. 이러한 로직은 요청의 권한을 파악하여 요청을 그다음으로 넘기거나 거부하는 로직이므로 가드를 사용하면 될 것 같습니다. 그런데 가드는 어떻게 달라지는 정보("admin"과 "user" 모두 가능, "admin"만 가능)를 파악할 수 있을까요?

여기서 custom metadata가 작동합니다. Nest는 @SetMetadata() 데코레이터를 통해 경로 핸들러에 사용자 정의 메타데이터를 첨부하는 기능을 제공합니다. 이 메타데이터는 가드가 결정을 내리는 데 필요한 누락된 데이터를 제공합니다. 아래 @SetMetadata() 사용을 살펴보겠습니다.

 

더보기

물론 메서드가 요구하는 권한의 정도가 달라질 때마다 또 다른 가드를 만들어서 메서드에 해당 가드를 설정해도 되지만, 그것은 굉장한 코드 중복입니다. 비유를 하자면 그것은 두 수를 더하는 더하기 함수를 만드는 데 <1번>처럼 모든 것을 허용하는 함수를 만드는 것이 아니라 <2번>처럼 모든 수에 대한 함수를 각각 만드는 것입니다.

 

<1 번>

function add(x, y) {
	return x + y;
}

 

<2 번>

function add(1, 0) {
	return 1 + 0;
}

function add(1, 1) {
	return 1 + 1;
}

function add(1, 2) {
	return 1 + 2;
}

function add(1, 3) {
	return 1 + 3;
}

function add(2, 9) {
	return 2 + 9;
}

......

 

<cats.controller.ts>

@Post()
@SetMetadata('roles', ['admin'])
async modify(@Body() modifyCatDto: ModifyCatDto) {
	this.catsService.modify(modifyCatDto);
}


위 구성에서 우리는 "roles"라는 메타데이터(roles는 key이고 [admin]은 특정 value임)를 create() 메서드에 첨부해 줬습니다. 하지만 @SetMetadata()를 직접 사용하는 것은 좋지 않습니다. 그것보다는 아래와 같이 고유한 데코레이터를 만드는 것이 좋습니다. 아래의 구조가 훨씬 깨끗하고 읽기 쉬우며 실수를 줄입니다. (기능은 똑같지만 구조상 아래의 구조가 더 낫다는 것)

 

<roles.decorator.ts>

import { SetMetadata } from '@nestjs/common'
			
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
			


<cats.controller.ts>

@Post()
@Roles('admin')
async create(@Body() createCatDto: CreateCatDto) {
	this.catsService.create(createCatDto);
}

 

Gard에게 metadata를 전달해 줬으니 이제 Guard에서 해당 메타데이터를 받아봅시다. 그러기 위해서는 Reflector라는 도우미 클래스가 필요합니다.

 

<auth.guard.ts>

import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';

@Injectable()
export class AuthGuard implements CanActivate {
	constructor(private reflector: Reflector) {}

	 canActivate(context: ExecutionContext): boolean {
		const request = context.switchToHttp().getRequest();
		const roles = this.reflector.get<string[]>('roles', context.getHandler());
				
		if (!roles) {
        		// some logic ...
		}
		
       		// requset 헤더에 담긴 토큰을 추출
		// 해당 토큰을 검증
		// 토큰에 담긴 정보로부터 요청이 적절한 권한을 가졌는지 확인 ...
		// return boolean
	}
}


Reflector#get 메서드를 사용하면 메타데이터 키와 메타데이터를 검색할 콘텍스트(데코레이터 대상)라는 두 가지 인수를 전달하여 메타데이터에 쉽게 액세스 할 수 있습니다. 이 예에서 지정된 키는 'roles'입니다.(roles.decorator.ts 참조) 콘텍스트는 현재 처리된 경로 처리기(handler)에 대한 메타데이터를 추출하는 context.getHandler()를 통해 제공됩니다. getHandler()는 경로 처리기 함수에 대한 참조를 제공합니다.

만약, 컨트롤러 수준에서 메타데이터를 적용하여 컨트롤러 클래스의 모든 경로에 해당 메타데이터를 적용하고 싶다면 아래와 같이 작성하면 됩니다.

 

<cats.controller.ts>

@Roles('admin')
@Controller('cats')
export class CatsController {
	// some methods ...
}
			
            
   
<auth.guard.ts>
	...
	const roles = this.reflector.get<string[]>('roles', context.getClass());
	...

 

권한이 부족한 사용자가 엔드포인트를 요청하면 Nest는 자동으로 다음 응답을 반환합니다.

{
	"statusCode": 403,
	"message": "Forbidden resource",
	"error": "Forbidden"
}

 

가드가 false를 반환하면 프레임워크에서 ForbiddenException이 발생합니다. 다른 오류 응답을 반환하려면 고유한 특정 예외를 throw 하면 됩니다. 가드에 의해 발생한 모든 예외는 예외 레이어에서 처리됩니다.

 

 

** 제 개인 프로젝트에는 응답을 래핑 하는 Interceptor 클래스가 있습니다. 그런데 가드에서 발생한 오류는 어떻게 해도 Interceptor 클래스에서 포착하지 못하더라고요. 응답 래핑을 Interceptor 클래스에서만 하는 것이 깔끔하고 일관적이어서 기존에는 Interceptor 클래스에서만 응답 래핑을 하고 예외 계층에서는 예외 Log만 찍었었는데요. 가드에서 발생한 오류는 Interceptor 클래스를 경유하지 않으니 응답 래핑을 위해 예외 계층에 응답 래핑 코드를 추가했습니다. Interceptor에서 가드에서 발생시킨 예외를 잡으려고 엄청 시간을 쏟았는데, 여러분은 그러지 마세요... 혹시 제가 모르는 방법 다시 말해, 가드에서 발생시킨 예외를 인터셉터에서 잡을 수 있는 방법이 존재한다면 저에게 알려주세요!

 

요약

가드는 요청의 흐름으로 볼 때, 미들웨어와 인터셉터 사이에 존재하며 요청을 그다음으로 넘기거나 거부하는 역할을 한다.

가드에 추가적인 데이터를 넘겨주고 싶으면 @SetMetadata() 데코레이터를 사용하자.

 

 

 

 

 


https://docs.nestjs.com/guards#role-based-authentication

 

Documentation | NestJS - A progressive Node.js framework

Nest is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, is built with TypeScript and combines elements of OOP (Object Oriented Progamming), FP (Functional Programming), and FRP (Functional Reac

docs.nestjs.com