인증 및 인가의 기본 로직은 1. 접근 여부를 파악하고 2. 신원 정보를 확인하고 3. 일치하면 true
아니면 false
를 리턴하는 것입니다. 과정을 통과하지 못하는 경우 보통 권한 없음(401
) 또는 제한됨(403
) 에러를 반환합니다.
가드란?
가드는 CanActivate
인터페이스를 구현하며 싱글 리포지토리를 갖습니다. 가드는 요청을 조건에 따라 라우트 핸들러에서 처리 여부를 결정합니다. 이를 보통 인증이라 합니다.
Express 애플리케이션에서 인증은 보통 미들웨어에서 처리했습니다. 그러나 미들웨어는 next()
를 호출한 다음 어떠한 핸들러가 실행되는지 알지 못합니다. 반면, 가드는 ExecutionContext
인스턴스에 접근할 수 있으며, 다음에 실행될 것을 분명히 알고 있습니다.
가드를 통한 인증 구현
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()
함수를 구현해야 합니다.이는 불리언을 리턴해야 하며, 이를 통해 현재 요청을 허용할 것인지 말 것인지를 결정해야 합니다. 응답을 동기 또는 비동기(Promise
또는 Observable
을 통해)로 전달할 수 있습니다.
실행 컨텍스트
canActivate()
함수는 하나의 아규먼트인 ExecutionContext
를 받습니다. ExecutionContext
는 ArgumentsHost
를 상속 받습니다. 앞서 예외 처리 필터에서 ArgumentHost
를 살펴본 적이 있습니다. 위 예시에서 동일한 헬머 메소드를 사용하고 있으며, Request
객체의 레퍼런스를 받고 있습니다.
ArgumentsHost
를 확장하여 ExecutionContext
또한 현재 실행 과정에 추가 정보를 제공하는 새로운 헬퍼들을 추가할 수 있습니다. http를 사용하고 있다면 switchToHttp()
함수를 사용하여 요청을 판단하고 validateRequest
함수가 true
또는 false
를 리턴합니다.
실제 가드 구현
이제 보다 실용적인 가드를 구현해보겠습니다. 기본적인 가드 템플릿에서 시작해보겠습니다.
// roles.guard.ts
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 {}
핸들러에 역할 설정하기
우리의 RolesGuard
는 작동하지만 아직 그리 똑똑하지는 않습니다. 가드의 가장 중요한 기능 중 하나인 실행 컨텍스트의 장점을 사용하고 있지 않기 때문인데요. 현재까지는 역할에 대해 알지 못하며, 각 핸들러가 어떤 역할을 허용하는지도 모릅니다.
CatsController
의 경우 각 라우터에 대해 서로 다른 권한 스키마를 가질 수 있습니다. 어떤 것은 관리자에게 다른 어떤 것은 모두에게 개방되어 있을 것입니다. 이러한 역할을 라우터에 매칭시키는 유연하고 재사용가능한 방법은 무엇일까요?
사용자 메타데이터를 활용하면 됩니다. Nest는 @SetMetadata()
를 통해 라우트 핸들러에 사용자 메타데이터를 첨부할 수 있게 합니다.
// cats.controller.ts
@Post()
@SetMetadata('roles', ['admin'])
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
이제 우리는 roles
메타데이터터(roles
이 키이며 ['adamin']
은 값입니다) create
메소드에 첨부했습니다. 작동은 하겠지만 @SetMetadata()
를 라우트에 직접 사용하는 것은 좋은 생각은 아닙니다. 다음과 같이 데코레이터를 만들어 사용하는 것이 더 좋습니다.
// roles.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
데코레이터를 활용하는 방법이 더 깔끔하고 읽기에 편합니다. 이제 우리는 커스텀 @Roles
데코레이터를 갖게 되었으며 이를 다음과 같이 사용할 수 있습니다.
// cats.controller.ts
@Post()
@Roles('admin')
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
최종 정리
이제 RolesGuard
와 관련된 내용을 모두 정리해보도록 하겠습니다. 현재 이는 모든 경우에 true
를 리턴하며 모든 요청을 처리합니다.
현재 사용자에게 할당된 역할에 따라 조건을 비교하여 요청 처리를 조건화하고자 한다면, Reflector
헬퍼 클래스를 사용할 수 있습니다.
// roles.guards.ts
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);
}
}
참고 자료
'개발 > NestJS' 카테고리의 다른 글
NestJS 기초 (13) JWT를 사용한 인증 인가 처리와 데코레이터 구현 (0) | 2022.11.01 |
---|---|
NestJS 기초 (11) API 문서 작성하기 (스웨거) (0) | 2022.10.10 |
NestJS 기초 (10) 파이프와 유효성 검사 (0) | 2022.10.09 |