Role Guard 생성 __(Nest 공식문서 번역)

DatQueue·2022년 11월 3일
4

NestJS _TIL

목록 보기
3/12
post-thumbnail

시작하기에 앞서

이번 포스팅은 애초 계획엔 없었으나 Nest를 이용한 "JWT 생성부터 권한관리" 중 Role Guard를 생성하는 과정에 조금 더 필요한 선수 지식과 어떠한 과정을 베이스로 진행하느냐에 관해 알아보기 위해 작성하게 된다.

공식문서에서 제시하는 Role Guard의 작성법과 Roles Decorator를 왜 따로 작성하는지에 대해 보다 근본적인 이유를 알 수 있을 것이다. 클린 코드로 가는 리펙토링 과정이라 봐도 좋다.

진행 내용은 오로지 Nest 공식 문서번역하는 과정으로 진행할 것이다.


공식 문서 번역


Role-based authentication

Let's build a more functional guard that permits access only to users with a specific role. We'll start with a basic guard templete, and build on it in the coming sections. For now, it allows all requests to proceed:

( 특정 역할을 가진 사용자에게만 접근을 허용하는 좀 더 기능적인 가드를 만들어 봅시다. 우리는 기본적인 가드 템플릿으로 시작하여 다음 섹션에서 이를 기반으로 수행할 것입니다. 지금부터 이 템플릿은 진행하고자 하는 모든 요청에 사용됩니다. )

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

Building guards

Like pipes and exception filters, guards can be controller-scoped, method-scoped, or global-scoped. Below, we set up a controller-scoped guard using the @UseGuards() decorator. This decorator may take a single argument, or a comma-separated list of arguments. This lets you easily apply the appropriate set of guards with one declaration.

( 파이프 및 예외 필터와 같이, 가드는 controller-scoped, method-scoped 혹은 global-scoped 일 수 있습니다. 아래에서(아래 코드 참조), @UseGuards() 데코레이터를 사용하여 컨트롤러 범위 가드를 설정합니다. 이 데코레이터는 단일 인자 또는 쉼표로 구분된 배열 리스트 인자를 가질 수 있습니다. 이를 통해 단일 선언으로 적절한 가드 세트를 쉽게 적용할 수 있습니다. )

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

HINT
The @UseGuards() decorator is imported from the @nestjs/common package.

Above, we passed the RoleGuard type (instead of an instance), leaving responsibility for instantiation to the framework and enabling dependency injection. As with pipes and exception filters, we can also pass an in-place instance:

( 위에서 우린 인스턴스 대신 RoleGuard 유형을 전달하여 프레임워크에 인스턴스화에 대한 책임을 맡기고 의존성 주입을 활성화 하였습니다. 파이프 및 예외 필터와 마찬가지로 내부 인스턴스도 전달할 수 있습니다. )


In order to set up a global guard, use the useGlobalGuards() method of the Nest application instance:

( 전역 가드를 설정하기 위해선, Nest 애플리케이션 인스턴스의 useGlobalGuards() 메서드를 사용하세요. )

const app = await NestFactory.create(AppModule);
app.useGlobalGuards(new RolesGuard());

Global guards are used across the whole application, for every controller and every route handler. In terms of dependency injection, global guards registered from outside of any module (with useGlobalGuards() as in the example above) cannot inject dependencies since this is outside the context of any module. In order to solve this issue, you can set up a guard directly from any module using the follwing construction.

( 전역 가드는 모든 컨트롤러와 모든 라우트 핸들러에 대해 전체 애플리케이션에서 사용됩니다. 의존성 주입(DI)의 측면에서 모듈(위의 예제와 같이 useGlobalGuards()가 있는) 외부에서 등록된 전역 가드는 종속성을 주입할 수 없습니다. 이는 모듈 컨텍스트 외부에서 수행되기 때문입니다. 이 문제를 해결하기 위해 다음 구조를 사용하여 모든 모듈에서 직접 가드를 설정할 수 있습니다. )

// app.module.ts

import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';

@Module({
  providers: [
    {
      provide: APP_GUARD,
      useClass: RolesGuard,
    },
  ],
})
export class AppModule {}

Setting roles per handler

( 핸들러별 역할 설정 )


Our RolesGuard is working, but it's not very smart yet. We're not yet taking advantage of the most important guard feature - the execution context. It doesn't yet know about roles, or which roles are allowed for each handler. The CatsController, for example, could have different permission schemes for different routes. Some might be available only for an admin user, and others could be open for everyone. How can we match roles to routes in a flexible and reusable way?

( 우리의 RolesGuard는 작동하고 있지만 아직 그리 똑똑하진 않습니다. 우리는 아직 가장 중요한 가드의 특징인 실행 컨텍스트를 활용하지 않고 있습니다. 아직 역할이나 각 핸들러에 허용되는 역할에 대해 알지 못합니다. 예를 들어 CatsController는 다른 경로에 대해 다른 권한 스킴을 가질 수 있습니다. 일부는 관리자만 사용할 수 있고 다른 일부는 모든 사람에게 공개될 수 있습니다. 어떻게 우린 유연하고 재사용 가능한 방식으로 라우트(경로)에 역할을 매칭시킬 수 있을까요? )


This is where custom metadata comes into play. Nest provides the ability to attach custom metadata to route handlers through the @SetMetadata() decorator. This metadata supplies our missing role data, which a smart guard needs to make decisions. Let's take a look at using @SetMetadata():

( 여기에서 사용자 정의 메타데이터가 작동합니다. Nest는 @SetMetadata() 데코레이터를 통해 라우트 핸들러에 커스텀 메타데이터를 첨부하는 기능을 제공합니다. 이 메타데이터는 스마트 가드가 결정을 내리는 데 필요한 누락된 역할 데이터를 제공합니다. 그럼 이제 @SetMetadata()를 사용하는 방법을 살펴보겠습니다. )


간단하게 메타데이터에 관해 알아보자. 예를 들어 admin(관리자)만 사용가능도록 제한을 두고 싶다고 해보자. create메서드는 (POST 방식) admin만 사용가능하다는 것을 어디선가 알고 있어야 한다. 어디선가 알고 있어야 하는 정보, 이를 "Metadata(메타데이터)"라고 한다. 즉, "create 메서드는 admin 역할일 때만 호출되어야 한다"고 하는 메타데이터를 위에서 제시한 @SetMetadata() 데코레이터로 지정할 수 있는 것이다.


// cats.controller.ts

@Post()
@SetMetadata('roles', ['admin'])
async create(@Body() createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);
}

참고로 SetMetadata는 메타데이터를 key와 value로 받아 CustomDecorator 타입으로 돌려주는 데코레이터이다.

export declare const SetMetadata: <K = string, V = any>(metadataKey: K, metadataValue: V) => CustomDecorator<K>;

HINT
The @SetMetadata() decorator is imported from the @nestjs/common pacakage.

With the construction above, we attached the roles metadata (roles is a key, while [admin] is a particular value) to the create() method. While this works, it's not good practice to use @SetMetadata() directly in your routes. Instead, create your own decorators, as shown below:

( 위의 구조를 통해 roles 메타데이터(roles는 key이고 [admin]은 특정 value)를 create() 메서드에 첨부했습니다. 이것이 작동하는 동안 경로에서 직접 @SetMetadata()를 사용하는 것은 좋은 습관이 아닙니다. 대신 아래와 같이 자신만의 데코레이터를 생성합니다 : )

// roles.decorator.ts

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

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

This approach is much cleaner and more readable, and is strongly typed. Now that we have a custom @Roles decorator, we can use it to decorate the create() method.

( 이 접근 방식은 훨씬 깨끗하고 읽기 쉽고, 엄격한 자료형(강 타입) 입니다. 이제 커스텀 @Roles 데코레이터가 있으므로, 이를 사용하여 create() 메서드에 데코레이팅 시킬 수 있습니다. )


Putting it all together

Let's now go back and tie this together with our RolesGuard. Currently, it simply returns true in all cases, allowing every request to proceed. We want to make the return value conditional based on the comparing the roles assigned to the current user to the actual roles required by the current route being processed. In order to access the route's role(s) (custom metadata), we'll use the Reflector helper class, which is provided out of the box by the framework and exposed from the @nestjs/core package.

( 이제 돌아가서 이것을 RolesGuard와 연결해 보겠습니다. 현재는, 모든 경우에 true를 반환하여 모든 요청을 계속 진행할 수 있습니다. 우리는 현재 유저에게 할당된 역할을 처리 중인 현재 경로에 필요한 실제 역할과 비교하여 반환 값을 조건부로 만들고 싶습니다. 경로의 역할 (커스텀 메타데이터)에 접근하기 위해서, 우린 프레임워크에서 기본 제공되고 @nestjs/core 패키로부터 노출되는 Reflector 도우미 클래스를 사용할 것입니다. )

// roles.guard.tsJS

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

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}  // 가드에 Reflector를 주입

  canActivate(context: ExecutionContext): boolean {
    // 가드에 주입받은 Reflector를 이용하여 메타데이터 리스트를 얻는다.
    const roles = this.reflector.get<string[]>('roles', context.getHandler());
    if (!roles) {
      return true;
    }
    // 실행 컨텍스트로부터 request 객체를 얻고, request 객체에 포함된 user 객체를 얻는다.
    const request = context.switchToHttp().getRequest();
    const user = request.user;
    return matchRoles(roles, user.roles);
  }
}

HINT
In the node.js world, it's common practice to attach the authorized user to the request object. Thus, in our sample code above, we are assuming that request.user contains the user instance and allowed roles. In your app, you will probably make that association in your custom authentication guard(or middleware).

( node.js에서는 승인된 사용자를 request 객체에 연결하는 것이 일반적인 방법입니다. 따라서 위의 샘플 코드에서는 request.user에 사용자 인스턴스와 허용된 역할이 포함되어 있다고 가정합니다. 당신의 앱에서, 커스텀 인증 가드(또는 미들웨어)에서 해당 연결을 만들 것입니다.)

Warning
The logic inside the matchRoles() function can be as simple or sophisticated as needed. The main point of this example is to show how guards fit into the request/response cycle.

( matchRoles()함수 내부의 로직은 필요에 따라 간단하거나 정교할 수 있습니다. 이 예제의 요점은 가드가 요청/응답 주기에 어떻게 맞추는지 보여주는 것입니다. )


When a user with insufficient privileges requests an endpoint, Nest automatically returns the following response:

( 권한이 불충한 사용자가 엔드포인트를 요청할 때, Nest는 다음 응답을 자동으로 반환합니다: )

throw new UnauthorizedException();

Any exception thrown by a guard will be handled by the exceptions layer (global exceptions filter and any exceptions filters that are applied to the current context).

( 가드에서 던전 모든 예외는 exceptions layer(전역 예외 필터 및 현재 컨텍스트에 적용된 모든 예외 필터)에서 처리됩니다. )


생각정리

영어로 된 공식문서를 찾아보게 된 경험은 그리 많지 않다. 그러다 보니 정확하고 근본적인 코드 작성에 대해 항상 어려움을 겪은적이 많았던 것 같다. 이번 포스팅을 작성하면서 Nest 공식문서를 살펴보게 되었고 베이스가 되는 코드및 진행 방법에 있어 도움을 받게 되었다. 동시에 왜 프로그래머는 "영어"라는 언어에 익숙해져야 하는가에 대해 알게되었다.

profile
You better cool it off before you burn it out / 티스토리(Kotlin, Android): https://nemoo-dev.tistory.com

1개의 댓글

comment-user-thumbnail
2023년 8월 9일

공식 문서를 보면서 막막했는데 번역 해주신 글 잘 보고 갑니다!ㅎㅎ

답글 달기