nest.js

[고스락 티켓 2.0] nest js 유저 role 기반 api 인가

ImNM 2022. 8. 13. 03:23

 어노테이션과 메타데이터를 이용해서 , 유저가 Admin인지 일반 User인지에따른 api 인가를 설정해보도록 하자.

 

이글을 통해서 얻어갈 수 있는점

  • nestjs SetMetadata 를 통해서 어노테이션과, 메타데이터 설정
  • 가드에서 accessToken의 유저정보를 확인후에 접근 제한 하기
  • getAllAndOverride 메서드를 통해 클래스 레벨과 메소드 레벨 중 메소드 레벨에 우선순위두기

 

[고스락 티켓 2.0] 두번째 프로젝트는 어떻게 달라졌을까요?

우선 고스락 티켓 예매 프로젝트의 목적은 기존 종이티켓으로 표를 팔러다니던 OB시절에서 정산의 어려움이나 공연 홍보의 어려움 또한 비대면 시대로 돌입하면서 학교에 모이는 인원이 적다보

devnm.tistory.com

이 글의 내용들은 고스락 티켓예매 프로젝트(이하 고티켓) 에서 진행하는 백엔드에서 적용하고 사용중인 소스들의 예시이다.

 

 1.0 버전의 ( 21번째 공연 예매에선 ) 어드민 유저와 일반 유저를 아예 분리를 시켰는데 role 기반 인가 연습도 할겸해서 한 테이블 내에 유저의 권한을 두었다.

 관리자 페이지가 존재하고 , 주문 입금확인이나 조회등에서 관리자만 접근해야할 api들이 있다.

 

고티켓의 유저 모델에는 

@Entity()
export class User {
  @ApiProperty({
    description: '유저의 권한입니다.',
    enum: Role
  })
  @Expose()
  @Column({
    type: 'enum',
    enum: Role,
    default: Role.User
  })
  public role: Role;
  // 간략화된 소스입니다.
}

enum Role {
  User = 'User',
  Admin = 'Admin'
}

이렇게 유저정보에 role 정보를 저장하는데

 

  @ApiUnauthorizedResponse({
    status: 401,
    description: 'AccessToken 권한이 없을 경우'
  })
  @Roles(Role.Admin)
  @Delete('/:id/comment')
  async deleteComment(@Param('id') id: number)

위와 같은 방식으로 Role을 적어주면 접근할수있는 api에대해서 제한을 할수있다.

이제 적용하는 방법을 알아보

 

nestjs SetMetadata 를 통해서 어노테이션과, 메타데이터 설정


 

 

Documentation - Decorators

TypeScript Decorators overview

www.typescriptlang.org

메타데이터 나 데코레이터 부분은 공식문서를 한번읽으면 도움이 된다.

컴파일 타임에서 런타임으로 넘어갈때 적어놓은 어노테이션으로 메타데이터를 설정할 수 있는데

어떠한 클래스나 클래스의 프로퍼티에 키값을 가지고 정보를 저장 할 수 있다.

주의할점은 메타데이터가 한번 설정한뒤에 그뒤에 동적으로 변화할 순없다.

 

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

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

common/decorators/roles.decorator.ts

롤들을 인자로 받는다.  ...roles 는 @Roles("admin" ,"user" ) 처럼 이런형식으로 인자를 넘기면

["admin" ,"user" ] 로 변환을 해준다고 보면된다.

 

nest js 에서 제공해주는 SetMetadata를 활용해서 "roles" 라는 키에 권한 정보를 저장할 수 있다.

이렇게 컨트롤러 메서드나 컨트롤러 클래스에 적어주면 "누가 접근 할수 있는 메소드" 인지에 대한 정보를 얻을 수있다.

 

 

가드에서 인가 확인하기


import {
  CanActivate,
  ExecutionContext,
  ForbiddenException,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Role } from 'src/common/consts/enum';

@Injectable()
export class AccessTokenGuard implements CanActivate {
  constructor(private authService: AuthService, private reflector: Reflector) {}
  canActivate(
    context: ExecutionContext
  ): boolean | Promise<boolean> | Observable<boolean> {
    const request = context.switchToHttp().getRequest();
    return this.validateRequest(request, context);
  }

  private async validateRequest(request: Request, context: ExecutionContext) {
    // 커스텀 데코레이터로 만든 롤을 가져옴
    // 클래스와 함수 둘다 가져와야함.
    // 함수 레벨한테 우선순위를 줌 ( 스프링처럼 자세한 부분이 더 우선순위 높도록)
    const roles = this.reflector.getAllAndOverride<string[]>('roles', [
      context.getHandler(),
      context.getClass()
    ]);
    // accessToken payload 에서 유저 role 조회
    const user = this.authService.verifyAccessJWT(jwtString);
    // 적혀진 roles 정보가 없을때
    if (!roles) {
      return true;
    }
    // 적혀진 roles 정보 [] 일때  @Roles() 만준경우
    if (!roles.length) {
      return true;
    } 
    else {
      // 적힌 롤에 포함되는 권한을 가지고 있으면 통과
      if (roles.includes(user.role) === true) {
        return true;
      } 
      // 어드민이면 다통과
      else if (user.role === Role.Admin) {
        return true;
      } 
      // 모든케이스 위반시 403
      else {
        throw new ForbiddenException(
          AuthErrorDefine['Auth-3000'],
          '어드민 롤에 일반유저가 접근한 경우'
        );
      }
    }
  }
}

auth/guards/AccessToken.guard.ts

 

가드에서 제일 중요한점은 실행 컨텍스트를 가져올 수있다는 점인데.

excutionContext

현재클라이언트에서 요청한 api의 컨트롤러 클래스와 핸들러 ( 컨트롤러 메소드 ) 에 접근을 할 수있다는 점이다.

위에서 말했듯이 @Roles() 는 컨트롤러 클래스 에도 , 핸들러에도 적힐 수 있기 때문에

 ( 컨트롤러 클래스에 적히는경우 모든 해당 컨드롤러의 메소드를 admin레벨로 설정하는 등 ) 

 + 또한 실행컨텍스트를 통해서 클라이언트의 요청 헤더나 바디등에도 접근할수있다! 매우중요 ArgumentHost를 봐라!

 

네스트에서 주입받은 reflector를 사용하여 ( 가드 생성자에 있다 ) 현재의 실행되고있는 api의 클래스와 메소드 참조값을 넘긴다.

아마 내부적으론 reflect-metadata 활용해서 해당 참조값에 "roles"라는 키값을 가지고있는 메타데이터를 꺼내온다.

 

이때 getAllOverride 라는 메소드를 사용하고 있는데 이건 

우선순위를 생각해서 가령 ( admin 컨트롤러 내부에 진짜 최종 꼭대기 관리자인 superAdmin 이 써야하는 api 가 있다면 ) 

메소드를 최상위 우선순위로 두고 덮어쓰기를 해야할듯 싶어서 

 

const roles = this.reflector.getAllAndOverride<string[]>('roles', [
context.getHandler(),
context.getClass()
]);

위와 같은 코드를 작성했다.

 

nest 에서 제공하는 reflector

위는 우리가 생성자에서 주입받았던 리플렉터 인터페이스 이다 중요한부분은 저 targets 인데

 클래스의 인스턴스를 넘겨줘야만 ( 플레인 객체 넘기면 아무것도없다 ) 리플렉션을 이용해서 메타데이터를 빼내올 수 있다는 점이고

우리는 실행컨텍스트에서 api요청이왔을때 어느 컨트롤러인지, 어느 컨트롤러의 메소드가 실행중인지 에 대한 target 을 가져와야 메타데이터를 빼내올 수 있다는 점이다.

 

그 메타데이터는 컴파일타임에서 런타임으로 넘어갈때 정해진다는 점. 꼭 기억해두길바란다.

reflector 구현체

  구현체보면 뭐 리플렉트 메타데이터 써서 타겟에서 정보를 가져오고있다.

 

 

이렇게되면 

@controller("admin")
@Roles("admin")
class AdminClass {

  @Roles("superAdmin")
  @Delete('admin/:id')
  async deleteAdminUser(@Param('id') id: number)
  
  }

위에 예시일때 delete api는 superAdmin만 실행시킬수있다.

 

현재 그리고 @Roles() 가 적혀있지 않다면 모든 사람 ( 즉 일반유저도 ) 실행시킬수있게 구성해놨다.

간단하다.

 


 네스트 공식문서에는 setMatadata까진 나와있긴한데, role을 가지고 우선순위를 정해서 고티켓 프로젝트에 맞게 설정하는 구성을 해보았다. 내부적으로 어떻게 돌아가는지 이해하다보면 커스텀하기 점점 편해질 것이다.