Guards

Guards

Guard 는 CanActivate 인터페이스를 구현하는 @Injectable() 데코레이터가 달린 클래스이다.

Guard 는 request 가 실행 가능한지 여부를 판단한다. 흔히 이것을 authorization이라고 부르며 Express 에서는 대부분 midlleware 에서 처리하여 특정 컨트롤러와 강하게 연결되는것을 막았다.

그러나 middleware 에 가장 큰 문제는 next 이후에 어떤 함수가 실행되는지 모른다는 것에 있다. Guard 는 ExecutionContext 를 알수 있고 request/response 싸이클에 선언적으로 사용할 수 있어서 코드를 더 읽기좋고 선언적으로 만들어 준다.

Guard 는 각 미들웨어 이후에 실행되지만 인터셉터나 파이프 이전에 실행된다.

Authorization Guard

Authorization 은 Guard 에 가장 좋은 예이다. AuthGuard 는 request 에서 토큰을 추출하고 판단한 후 다음 과정을 진행할지 판단하게 할 수 있다.

auth.guard.ts :

1
2
3
4
5
6
7
8
9
10
11
12
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);
}
}

Guard 는 canActivate() 를 구현하고 request를 넘길지 여부를 boolean 으로 반환한다. 또한, Promise , Observable를 반환할 수 있기 때문에 동기적으로나 비동기 적으로나 반환하는것이 가능하다.

Execution context

canActivate 는 ExecutionContext 인스턴스를 받고 ExecutionContext 는 ArgumetnsHost 를 상속한다. context 변수를 통하여 request 를 얻을 수 있다.

Role-based authentication

특정 권한을 가진 유저만 허용하는 Guard 를 만들어 보자. 지금은 template 로서 모든 권한을 허용하고 추후에 기능을 추가하면서 role 기반 authentication 을 만들어 본다.

roles.guard.ts :

1
2
3
4
5
6
7
8
9
10
11
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;
}
}

Binding guards

guard 는 exception filter 와 마찬가지로 controller-scoped, method-scoped, global-scoped 모두 가능하다. 다음은 @UseGuards() 데코레이터를 이용하여 controller-scoped 로 사용한 예제이다. 해당 데코레이터는 단일 인수나 쉼표로 구분하여 인수를 받을 수 있다.

1
2
3
@Controller('cats')
@UseGuards(RolesGuard)
export class CatsController {}

내부 인스턴스를 전달할 수도 있다.

1
2
3
@Controller('cats')
@UseGuards(new RolesGuards())
export class CatsController {}

만약 method-scoped 로 사용하고 싶다면 UseGuards 데코레이터를 메서드 레벨에서 사용한다. 글로벌로 사용하고 싶다면 nest application 인스턴스에 useGlobalGuards() 메서드를 사용한다.

1
2
const app = await NestFactory.create(AppModule);
app.useGlobalGuards(new RolesGuard());

의존성 주입 측면에서 모듈 밖에서 useGlobalGuards() 메서드에 의해서 등록되므로 의존성을 주입할 수 없다. 이런 문제를 해결하기 위해서 module 주입해서 사용할 수 있다.

1
2
3
4
5
6
7
8
9
@Module({
providers: [
{
provide: APP_GUARD,
useClass: RolesGuard,
},
],
})
export class AppModule{}

Setting roles per handler

위에서 작성한 roleguard 는 지금 어떤 권한도 확인하지 않고 있다. 권한을 확인하는 가장 좋은 방법은 metadata를 활용하는 것이다. nest 는 custom meataData 를 첨부하는 @SetMetadata() 데코레이터를 제공한다.

cats.controller.ts :

1
2
3
4
5
6
@Post()
@SetMetadata('roles', ['admin'])
async create(@Body() createCatDto: CreateCatDto) {
this.catService.create(createCatDto);
}

이것이 작동하는 동안 경로에서 직접 @SetMetadata() 를 사용하는 것은 좋은 습관이 아니다. 대신 custom decorator 를 사용하도록 한다.

roles.decorators.ts :

1
2
3
import { SetMetadata } from '@nestjs/common';

export const Roles = (...roles: string[]) => SetMetadata('roles', roles);

이렇게 하면 읽기 쉽고 타입을 알 수 있는 custom decorator 가 만들어 진다. 사용은 다음과 같이 한다.

cats.controller.ts :

1
2
3
4
5
@Post()
@Roles('admin')
async create(@Body() createCatDto:CreateCatDto) {
this.catService.create(createCatDto);
}

Putting in all together

Guard 에 적용해보자. 먼저 권한 정보를 알아야하는데 우리는 metadata 를 통해서 권한정보를 첨부했다. 권한 정보에 접근 하기 위해서는 Reflector hleper class 를 사용한다.

roles.guard.ts :