개발일지

[NestJS] Guard, Decorator로 역할 기반 접근 제어(RBAC) 구현하기 본문

NestJS, Node.js/#02 NestJS

[NestJS] Guard, Decorator로 역할 기반 접근 제어(RBAC) 구현하기

lyjin 2024. 11. 10.

개요

인터넷 쇼핑몰이라고 가정한다면 상품 목록, 상세 보기 같이 모두가 접근할 수 있는 페이지가 있고 매출, 재고 관리 등 직원 전용 페이지가 있을 것이다. 이때 일반 유저는 직원 전용 페이지에 접근할 수 없어야한다. 같은 공지사항 페이지에서도 직원에게는 쓰기, 편집, 삭제 기능이 함께 노출되어야하고 일반 유저들은 읽기만 가능해야한다.

이러한 상황에서 RBAC를 사용하면 이러한 권한을 효율적이고 체계적으로 관리할 수 있다.

 

 

RBAC(Role-Based Access Control, 역할 기반 접근 제어)

사용자에게 역할을 부여하고 역할에 따라 리소스 접근 권한을 제어하는 메커니즘

 


RBAC 구현하기

일반 유저(user)와 직원(staff), 관리자(admin) 세가지 권한이 있다고 가정하자.

export enum Roles {
  User = 'user',
  Staff = 'staff',
  Admin = 'admin',
}

export const ROLES_KEY = 'roles';

 

 

Decorator 구현 - 접근 허용할 역할 설정하기

먼저 메타데이터를 설정하기 위한 데코레이터를 생성해야한다.

NestJS에 내장된 SetMetadata를 사용했다. Reflector.createDecorator를 사용할 수도 있다.

import { SetMetadata } from '@nestjs/common';

export const Roles = (...roles: ROLE[]) => SetMetadata(ROLES_KEY, roles);

 

 

다음과 같이 클래스(컨트롤러) 또는 핸들러 단위로 액세스 할 역할 정보를 지정해줄 수 있다.

// 클래스 단위
@Roles([Roles.Staff, Roles.Admin])
@Controller('/notice')
class NoticeController {
		...
}

// 핸들러 단위
@Post()
@Roles(Roles.Admin)
async writeNotice(@Body() dto) {
  await this.notiService.writeNotice(dto);
}

 

 

Guard 구현 - 접근 권한 확인하기

이제 해당 메타데이터를 가지고 유저 권한과 비교할 수 있어야 한다. NestJS의 가드를 사용해 기본 RBAC를 구현할 수 있다.

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

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

  async canActivate(context: ExecutionContext) {
    // 1. Reflector로 'roles'를 키값으로 하는 메타데이터 가져오기
    // handler(method) -> class(controller) 순으로 확인 (메서드 레벨 우선)
    const roles =
      this.reflector.get<string[]>(ROLES_KEY, context.getClass()) ||
      this.reflector.get<string[]>(ROLES_KEY, context.getHandler());

    // 2. roles 메타데이터 설정되지 않은 경우, 권한 확인 자체를 건너뜀
    if (!roles) {
      return true;
    }

    // 3. 해당 유저에게 부여된 역할(request.user.role) 확인
    const request = context.switchToHttp().getRequest();
    const userRole = request?.user?.role;
    
    // 4. 권한 없으면 접근 거부
    if (!userRole || !roles.includes(userRole)) {
      return false;
    }

    // 5. 권한 있으면 접근 허용
    return true;
  }
}

 

 

적용 방법은 일반 가드처럼 UseGuards를 사용하면 된다.

@Post()
@UseGuards(RoleGuard)
@Roles(Roles.Admin)
async writeNotice(@Body() dto) {
  await this.notiService.createNotice(dto);
}

 

 

사용자에게 역할 부여하기

그렇다면 유저 역할 request.user.role 은 어디서 부여해야하는걸까?

 

일반적으로 jwt 인증 가드를 구현할 때 디코딩된 payload를 request 객체에 저장한다.

@Injectable()
export class JwtGuard implements CanActivate {
  async canActivate(context: ExecutionContext): Promise<boolean> {
    // logic...
    const payload = this.jwt.verifyToken(token);
    request['user'] = payload; // 유저 정보 저장

    return true;
  }
}

 

 

즉, 로그인할 때 jwt payload 정보로 넣어주면 된다. request.user.role에 포함된 정보를 사용하면 서비스 로직에서도 핸들링 가능하다.

// service

async writeNotice(user, dto) {
  const { roles } = user;
	
  if (roles === Roles.Admin) {
    // ...
  } else if (roles === Roles.Staff) {
	  // ...
  } else {
    // ...
  }
}