메인으로 진행하던 프로젝트가 막바지일때 이야기이다.
우리 서비스의 기획 상 신고 기능이 있어야 했다.
그리고 이에 연동되는 유저에게 정지를 주는 방법은 아직 기획이 안된 상태였는데 여차저차 얘기를 해서 유저를 밴 시킬 수 있고 Admin 권한을 가진 유저만 접근 가능한 API를 별도로 만들기로 했다.
참조 링크:
Guards
에 대해서 알아보기 전에 밑의 사진을 먼저 보자.
해당 사진은 NestJS의 Request lifecycle
으로, 클라이언트로부터 요청이 들어오고 응답으로 나갈때 까지의 생명주기를 나타낸 사진이다.
더 자세한 설명은 별도의 포스트로 작성하도록 하고, 여기선 Middleware
, Guards
의 아주아주 간단한 특징, 일반적인 사용처와 왜 Guards
를 사용할 것인지만 설명하겠다.
사진을 보면 알겠지만 요청이 들어 왔을 때 가장 먼저 실행되는 것은 Middleware
이다.
보통은
Middleware
에서 파싱해주면 컨트롤러의 라우트 핸들러에서 매번 쿠키 파싱하는 번거로움과 중복 코드를 없앨 수 있음.Guards
를 사용하는 것을 추천Logger
Middlerware
다음으로 실행된다.
이름부터 대놓고 인증/인가를 처리하는 놈이라고 써있다.
다른건 제치고 Middleware
대신 Guards
를 통해 인증 / 인가 처리를 해야 하는 이유를 살펴보면
next
() 함수를 호출하고 어떤 핸들러가 실행될지 모름.Guards
는 ExecutionContext
(실행 컨텍스트) 인스턴스에 접근이 가능함.잘 이해가 안될 수도 있지만 일단 Guards를 통해 Admin access control 로직을 구현을 해보자.
목차만 보면
"잉? Admin만 접근 가능한 API 만든다며?"
할 수 있는데 단순 Admin을 위한 가드보단 우리가 직접 라우트 핸들러에 데코레이터로 매핑해서 지정해준 Roles에 유저의 role이 속해 있는지를 체크하는게 더 좋을 것 같다.
아무래도 나중에 유저의 권한에 대해서 더 세부적인 기획이 생기면 단순 Admin, User 외에도 더 많은 role이 생길텐데 그때그때마다 Guards
를 새로 구현해주는 것은 너무 비효율적 아니겠는가?
물론 NestJS 공식문서에서 예제로 있는 방법이기도 하다.
일단 해당 API로 요청을 보낸 유저의 Role이 우리가 접근 가능하도록 지정한 Roles에 해당하는지를 알아야 한다.
우리 서비스는 이미 JWT
를 이용한 Guard
, Passport
로 사용자 인증/인가를 해주고 request
객체에 Jwt에서 파싱한 user 객체를 프로퍼티로 붙여주고 있다(토큰 payload
에는 user의 id만 담겨있음).
그렇다면 Guards
에서 요청을 보낸 유저의 정보는 이미 request 객체를 통해 가져올 수 있으니 우리가 Controller
의 각각의 라우트 핸들러에서, 혹은 Controller
레벨에서 지정해준 Roles만 가져오면 된다.
이때 custom metadata가 중요한데 Reflector.createDecorator 혹은 내장된 SetMetadata 데코레이터를 이용하면 된다.
나는 SetMetadata를 이용하겠다.
import { SetMetadata } from '@nestjs/common';
import { ROLES_TOKEN } from '@src/admins/constants/roles.token';
import { UserRole } from '@src/users/constants/user-role.enum';
export const Roles = (...roles: UserRole[]): ClassDecorator & MethodDecorator =>
SetMetadata(ROLES_TOKEN, roles);
첫번째 인수로 내가 만들어놓은 Symbol인 ROLES_TOKEN을 키값으로 주고 해당 키값으로 가져올 metadataValue를 두번째 인수로 넣어준다.
그리고 어떤 역할을 위한 데코레이터인지 명확하게 보이기 위해 Roles라는 이름으로 새로 만들었다.
그리고
@Roles(UserRole.ADMIN)
export class AdminController {
}
이렇게 컨트롤러에 데코레이터를 달아주면 된다.
내가 컨트롤러에 달아준 이유는 어차피 Admin만 접근 가능한 엔드포인트밖에 없기 때문
이제 본격적으로 Guards 로직을 작성해보자.
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
@Injectable()
export class RoleClassGuard implements CanActivate {
constructor(
private readonly reflector: Reflector,
private readonly userService: UserService,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const { role } = await this.userService.findOneByOrNotFound({
where: { id: request.user.id },
});
const isRoleValid = this.getMetadata(context).includes(role);
if (!isRoleValid) {
throw new ForbiddenException('넌 못지나간다.');
}
return isRoleValid;
}
private getMetadata(context: ExecutionContext) {
return this.reflector.get<UserRole[]>(ROLES_TOKEN, context.getClass());
}
}
실제 프로젝트에 적용된 코드에서 아주 조금만 바꿨다.
일단 실행 순서는
JWT
의 payload
에 role도 추가해줬으면 이 과정을 생략해도 됨.Controller
)를 넘김.Controller
레벨에 해당 키값으로 등록된 metadata를 찾아서 return.controller, handler 레벨 별로 Guards
를 만들지 않고 RolesGuard 하나만 만들어서 둘 다 사용 가능하도록 코드를 짤 수는 있다.
그냥 최대한 다른거 몰라도 조금이라도 쉽게 이해할 수 있는 방향으로 글을 작성해봤다.