NestJS Role Guard 구현

백엔드·2023년 6월 17일
0

NestJS

목록 보기
3/7

들어가며

새로운 스프린트 기간이 시작되며, 새로운 기능 개발 요건이 들어왔습니다.

요구된 기능은 아래와 같습니다.

 1. 기존의 user에 저장된 role을 기반으로 API 접근을 차단하라

💻 기능 구현 예시

  • Role을 위한 custom decorator 구현
// roles.decorator.ts
import { SetMetadata } from "@nestjs/common";

// SetMetadata를 사용해 metadata 저장
export const Roles = (...roles: userRoles[]) => SetMetadata("roles", roles); 
  • Guard 구현
// handler.roles.guard.ts
@Injectable()
export class HandlerRolesGuard implements CanActivate {
  constructor(
    @InjectModel(OrganizationMember.name)
    private userModel: Model<userDocument>,
    private reflector: Reflector,
  ) {}

  async canActivate(ctx: ExecutionContext) {
    const request = ctx.switchToHttp().getRequest();
    const userId = request.session.credentials.user._id;

    const userRole = await this.getUserRole(userId);
    
    //metadata에 저장한 키를 통해 reflector에서 불러오기
    const roles = this.reflector.get<userRoles[]>("roles", ctx.getHandler()); 
   
    const hasRole = () => roles.includes(userRole);

    return userRole && hasRole();
  }

  private async getUserRole(
    userId: string,
  ): Promise<User["permission"]> {
    const user = await this.userModel.findOne({
      userId,
    });

    if (!user)
      throw new BadRequestException(
        `userId: ${userId} not exist in user`,
      );

    return user.permission;
  }
}
  • controller에서 사용
  @UseGuards(HandlerRolesGuard)
  @Patch("/:userId")
  @Roles(UserPermission.ADMIN)
  updateUser(
    @Body()
    updateUserDto: UpdateUserDto,
  ) {
      ...
  }

추상화

여러 권한 체계를 가진 환경이라면, AbstractPermissionGuard를 구현하여 해당 클래스를 상속받아 구체적인 권한 검사를 위한 가드를 만들 수 있습니다.

해당 클래스는 protected abstract 키워드를 사용하여 자식 클래스에서 반드시 구현되어야 하는 일부 메서드와 속성을 정의합니다.

export abstract class AbstractPermissionGuard implements CanActivate {
  protected abstract readonly reflector: Reflector;
  protected abstract get Service(): Service; // AbstractPermissionGuard를 상속받는 class에서 필요한 의존성을 주입할 수 있다

  constructor() {}

  protected async validateRole({ request, user, ctx }: AuthorizePayload) {
     // 공통적으로 사용될 수 있는 role validate 로직 구현
  }

  async canActivate(ctx: ExecutionContext) {
    const request = ctx.switchToHttp().getRequest();
    const user = request.session.credentials.user;
    return this.authorize({ request, user, ctx });
  }
 
//  AbstractPermissionGuard를 상속받는 class에서 필요에 맞게 authorize를 구현합니다.
  protected abstract authorize(
    args: AuthorizePayload,
  ): boolean | Promise<boolean>;
}

추상화를 통해 얻는 이점

코드의 재사용성

공통적으로 사용되는 role validate 로직을 validateRole method로 구현하여 다른 여러 라우터나 컨트롤러에서 동일한 권한 검사를 필요로 할 때 중복된 코드를 작성하지 않아도 됩니다. 이를 통해 코드의 재사용성을 높일 수 있습니다.

유지 보수성

AbstractPermissionGuard를 추상화하여, 필요에 따라 자식 클래스에서 구체적인 로직을 구현하기 때문에 향후 변경이나 추가 요구사항이 있을 때도 해당 로직만 수정하면 되므로 유지보수가 용이해집니다.

확장성

AbstractPermissionGuard를 상속하여 구현된 커스텀 가드는 기존 로직을 기반으로 새로운 권한 검사를 추가하거나 수정할 수 있습니다. 새로운 권한 체계나 권한 그룹을 추가할 때 AbstractPermissionGuard의 기능을 확장하여 해당 권한 검사를 수행할 수 있습니다.

마치며

⭐️ 위의 예시와 같이 구현하여, 지정된 role이 아닌 사용자가 특정 API에 접근 할 경우, guard에서 forbidden을 띄우고 접근을 차단하여 role을 기반으로 API 접근을 차단 기능을 구현하였습니다.

profile
백엔드 개발자

0개의 댓글