특정 역할에 해당하는 유저만 권한을 허락하는 guard 와 Jwt-guard 함께 사용하는 법

chanykim·2021년 11월 14일
0

참고
https://docs.nestjs.kr/guards
https://docs.nestjs.kr/fundamentals/execution-context

Guards

  • @Injectable() 데코레이터로 주석이 달린 클래스이다.
  • CanActivate 인터페이스를 구현해야 한다.
  • 모든 가드는 canActivate() 함수를 구현해야 한다.
    - true를 반환하면 요청이 처리됩니다.
    - false를 반환하면 Nest는 요청을 거부합니다.

Role-based authentication

기본 가드 템플릿

//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 {}

JwtAuth와 함께 사용

여기서 JwtAuthGuard를 만들었다면 @UseGuards() 데코레이터에 두개를 넣어 사용 가능하다.

@Controller('cats')
@UseGuards(JwtAuthGuard, RolesGuard)
export class CatsController {}

Setting roles per handler

Nest는 @SetMetadata() 데코레이터를 통해 라우트 핸들러에 커스텀 메타데이터를 첨부하는 기능을 제공한다.

@SetMetadata('roles', ['admin'])
  • 이런 복잡한 데코에이터보다는 라우트에서 직접 @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);
}

Putting it all together

현재 사용자에게 할당된 역할을 처리중인 현재 라우트에 필요한 실제 역할과 비교하여 반환값을 조건부로 만들고 싶다.
라우트의 역할(커스텀 메타데이터)에 액세스하기 위해 Reflector 헬퍼 클래스를 사용한다.

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);
  }
}

matchRoles 함수는 @Roles 데코레이터로 받아온 값과 리퀘스트된 user에 저장된 roles를 비교하여 bool값으로 나타내는 함수로 간단히 나타낼 수 있다.

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

Reflector의 get 메소드를 사용하면 메타데이터를 검색할 메타데이터 키 및 컨텍스트(데코레이터 대상)의 두 인수를 전달하여 메타데이터에 쉽게 액세스할 수 있다.
이 예에서 지정된 키는 roles 이다.
컨텍스트는 context.getHandler() 호출에 의해 제공되며, 결과적으로 현재 처리된 라우트 핸들러에 대한 메타데이터가 추출된다.

프로젝트 이슈와 해결

도서관 홈페이지를 만들고 있는데 현재 개발과정에선 사서이든 아니든 대출, 반납이 가능한 페이지에 접근가능한 상태였다.
도서관 대출과 반납은 사서의 권한이므로 홈페이지 로그인 권한 이외에 사서만 대출, 반납 페이지에 들어가는 가드를 만들어 걸어줘야 했다.

그리고 위와 같은 공식 홈페이지를 참고하여 만들었다.
데코레이터를 만드는데 SetMetadata를 사용할 때 따로 파일을 만들어

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

이런 식으로 좀 더 세련되게 만드는 법을 알 수 있었다.

//roles.guard.ts
@Injectable()
export class RolesGuard implements CanActivate {
  constructor(
    private reflector: Reflector,
    private userService: UsersService,
  ) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const roles = this.reflector.get<string[]>('roles', context.getHandler());
    if (!roles) {
      return false;
    }
    const request = context.switchToHttp().getRequest();
    const user = await this.userService.userFind(request.user.login);
    console.log(user);
    if (roles[0] === 'admin' && user.librarian) return true;
    return false;
  }
}
//lendings.controller.ts
@Controller('lendings')
@UseGuards(JwtAuthGuard, RolesGuard)
export class LendingsController {
  constructor(private readonly lendingsService: LendingsService) {}
  
  @Get()
  @Roles('admin')
  async mainpage() {
    return 'hello!';
  }
   [...]
  
}

이런 식으로 코드를 작성하여 Test를 했는데 roles.guard.ts에서 DB에 유저 정보를 가져와 사서인지 확인하는 식이라 굳이 Roles를 넣어야하나? 라는 의문이 들었다.

없어도 상관없는 것 같은 생각도 드는데 대출,반납 controller에 @UseGuards(JwtAuthGuard, RolesGuard) 만 넣으면 되지 굳이 다른 메서드에 @Roles('admin') 이런 데코레이터를 붙어야 할지 잘 모르겠다.

나중에 좀 더 버전이 업그레이드 될 때 등급 시스템이 도입이 된다면 활용할 수 있을텐데 현재 버전에서는 사용하지 않아도 될 것 같기도 하다.
여튼 이슈 해결!

profile
오늘보다 더 나은 내일

0개의 댓글