Guards

Guards

가드는 @Injectable() 데코레이터로 주석이 달린 클래스입니다. 가드는CanActivate 인터페이스를 구현해야합니다.

가드는 단독 책임이 있습니다. 런타임에 존재하는 특정 조건 (예: 권한, 역할, ACL 등)에 따라 지정된 요청이 라우트 핸들러에 의해 처리될 지 여부를 결정합니다. 이를 종종 권한이라고합니다. 인증 (및 일반적으로 공동 작업 인 인증)은 일반적으로 기존 Express 응용 프로그램의 미들웨어에 의해 처리되었습니다. 토큰 유효성 검사 및 속성을 요청(Request)개체에 연결하는 것과 같은 것은 특정 경로 컨텍스트 (및 해당 메타 데이터)와 강력하게 연결되어 있지 않기 때문에 미들웨어는 인증에 적합한 선택입니다.

그러나 미들웨어는 본질적으로 바보입니다. next()함수를 호출한 후 어떤 핸들러가 실행될지 알 수 없습니다. 반면에 GuardsExecutionContext 인스턴스에 액세스할 수 있으므로 다음에 무엇이 실행 될지 정확히 알 수 있습니다. 요청/응답 주기의 정확한 시점에 처리 로직을 삽입하고 선언적으로 처리할 수 있도록 예외 필터, 파이프 및 인터셉터와 같이 설계되었습니다. 이렇게 하면 코드를 건조하고 선언적으로 유지할 수 있습니다.

info 힌트 가드는 각 미들웨어 에 실행되지만, 인터셉터 나 파이프는 에 실행됩니다.

Authorization guard

언급 한 바와 같이 권한은 발신자 (일반적으로 특정 인증된 사용자)에게 충분한 권한이 있는 경우에만 특정 경로를 사용할 수 있어야 하기 때문에 Guards의 훌륭한 사용 사례입니다. 우리가 구축 할 AuthGuard는 이제 인증된 사용자를 가정합니다 (따라서 토큰이 요청 헤더에 첨부되어 있음). 토큰을 추출하고 유효성을 검사하고 추출된 정보를 사용하여 요청을 진행할 수 있는지 여부를 결정합니다.

@@filename(auth.guard)
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';

@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    const request = context.switchToHttp().getRequest();
    return validateRequest(request);
  }
}
@@switch
import { Injectable } from '@nestjs/common';

@Injectable()
export class AuthGuard {
  async canActivate(context) {
    const request = context.switchToHttp().getRequest();
    return await validateRequest(request);
  }
}

validateRequest() 함수 내부의 논리는 필요한 만큼 간단하거나 정교할 수 있습니다. 이 예의 요점은 보호자가 요청/응답 주기에 어떻게 맞는지 보여줍니다.

모든 가드는 canActivate()함수를 구현해야합니다. 이 함수는 현재 요청이 허용되는지 여부를 나타내는 조건값을 반환해야 합니다. 응답을 동기식 또는 비동기식으로 반환 할 수 있습니다 (Promise 또는 Observable을 통해). Nest는 리턴 값을 사용하여 다음 조치를 제어합니다.

  • true를 반환하면 요청이 처리됩니다.

  • 'false'를 반환하면 Nest는 요청을 거부합니다.

canActivate()함수는 ExecutionContext인스턴스라는 단일 인수를 취합니다. ExecutionContextArgumentsHost에서 상속받습니다. 예외 필터 챕터에서 앞서 ArgumentsHost를 보았습니다. 여기서는 original 핸들러로 전달된 인수를 감싸는 래퍼이며 응용 프로그램의 유형에 따라 다른 인수 배열을 포함합니다. 이 주제에 대한 자세한 내용은 예외 필터 챕터을 다시 참조하십시오.

Execution context

ExecutionContextArgumentsHost를 확장하여 현재 실행 프로세스에 대한 추가 정보를 제공합니다. 그 모습은 다음과 같습니다.

export interface ExecutionContext extends ArgumentsHost {
  getClass<T = any>(): Type<T>;
  getHandler(): Function;
}

getHandler()메소드는 호출될 핸들러에 대한 참조를 리턴합니다. getClass()메소드는 이 특정 핸들러가 속하는 Controller 클래스의 유형을 리턴합니다. 예를 들어, 현재 처리 된 요청이 CatsController에서 create()메소드로 지정된 POST 요청 인 경우, getHandler()create() 메소드에 대한 참조를 리턴하고 getClass()CatsController type (인스턴스 아님)을 반환합니다.

Role-based authentication

특정 역할을 가진 사용자만 액세스 할 수 있는 보다 기능적인 보호 기능을 구축해 보겠습니다. 기본 가드 템플릿으로 시작하여 다음 섹션에서 작성합니다. 현재로서는 모든 요청을 진행할 수 있습니다.

@@filename(roles.guard)
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;
  }
}
@@switch
import { Injectable } from '@nestjs/common';

@Injectable()
export class RolesGuard {
  canActivate(context) {
    return true;
  }
}

Binding guards

파이프 및 예외 필터와 마찬가지로 가드는 컨트롤러 범위(controller-scoped), 방법 범위(method-scoped) 또는 전역 범위(global-scoped) 일 수 있습니다. 아래는 @UseGuards()데코레이터를 사용하여 컨트롤러 범위의 보호대를 설정했습니다. 이 데코레이터는 단일 인수 또는 쉼표로 구분된 인수 목록을 사용할 수 있습니다. 이를 통해 한 번의 선언으로 적절한 가드 세트를 쉽게 적용할 수 있습니다.

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

info 힌트 @UseGuards()데코레이터는 @nestjs/common 패키지에서 가져옵니다.

위에서 우리는 인스턴스 대신에 롤 가드 (RolesGuard) 유형을 전달하여 프레임 워크에 인스턴스화와 의존성 주입을 가능하게 했습니다. 파이프 및 예외 필터와 마찬가지로 내부 인스턴스를 전달할 수도 있습니다.

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

위의 구성은이 컨트롤러가 선언한 모든 핸들러에 가드를 연결합니다. 가드가 단일 메소드에만 적용되도록 하려면 메소드 수준에서 @UseGuards()데코레이터를 적용합니다.

전역 가드를 설정하려면 Nest 애플리케이션 인스턴스의 useGlobalGuards()메소드를 사용하십시오.

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

warning 알림 하이브리드 앱의 경우 useGlobalGuards()메소드는 게이트웨이 및 마이크로 서비스에 대한 보호를 설정하지 않습니다. "표준"(하이브리드가 아닌) 마이크로 서비스 앱의 경우 useGlobalGuards()는 가드를 전역으로 마운트합니다.

전역 가드는 모든 컨트롤러와 모든 경로 처리기에 대해 전체 응용 프로그램에서 사용됩니다. 의존성 주입의 관점에서, 모듈 외부에서 등록된 전역 가드 (위의 예에서와 같이 useGlobalGuards()로)는 의존성이 주입될 수 없습니다. 이는 모듈의 컨텍스트 밖에서 수행되기 때문입니다. 이 문제를 해결하기 위해 다음 구성을 사용하여 모든 모듈에서 직접 가드를 설정할 수 있습니다.

@@filename(app.module)
import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';

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

info 힌트 가드에 대한 의존성 주입을 수행하기 위해이 접근 방식을 사용할 때, 이 구성이 사용되는 모듈에 관계없이, 가드는 실제로 전역입니다. 어디에서 해야 합니까? 가드 (위 예에서RolesGuard)가 정의된 모듈을 선택하십시오. 또한 커스텀 프로 바이더 등록을 다루는 유일한 방법은 useClass가 아닙니다. 여기에 대해 자세히 알아보십시오.

Reflection

우리의 RolesGuard가 작동하고 있지만 아직 똑똑하지는 않습니다. 우리는 아직 가장 중요한 보호 기능인 실행 컨텍스트(execution context)를 활용하지 않습니다. 아직 역할이나 각 처리기에 허용되는 역할에 대해서는 아직 알지 못합니다. 예를 들어 CatsController는 경로마다 다른 권한 체계를 가질 수 있습니다. 일부는 관리자만 사용할 수 있고 다른 일부는 모든 사람에게 공개될 수 있습니다. 유연하고 재사용 가능한 방식으로 역할과 경로를 어떻게 일치시킬 수 있습니까?

사용자 지정 메타 데이터가 사용되는 곳입니다. Nest는 @SetMetadata()데코레이터를 통해 핸들러를 라우트하기 위해 커스텀 메타 데이터를 첨부하는 기능을 제공합니다. 이 메타 데이터는 스마트 가드가 결정을 내려야 하는 누락된 역할 데이터를 제공합니다. @SetMetadata()를 사용하여 봅시다 :

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

info 힌트 @SetMetadata()데코레이터는@nestjs/common 패키지에서 가져옵니다.

위의 구성에서 우리는 roles 메타 데이터 (roles 가 핵심이고['admin']이 특정 값임)를 create()메소드에 첨부했습니다. 이것이 작동하는 동안 경로에서 직접 @SetMetadata()를 사용하는 것은 좋지 않습니다. 대신 아래와 같이 자신 만의 데코레이터를 만드십시오.

@@filename(roles.decorator)
import { SetMetadata } from '@nestjs/common';

export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
@@switch
import { SetMetadata } from '@nestjs/common';

export const Roles = (...roles) => SetMetadata('roles', roles);

이 접근 방식은 훨씬 깨끗하고 읽기 쉽고 강력하게 입력됩니다. 이제 커스텀 @Roles()데코레이터가 있으므로 이를 사용하여 create()메소드를 장식 할 수 있습니다.

@@filename(cats.controller)
@Post()
@Roles('admin')
async create(@Body() createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);
}
@@switch
@Post()
@Roles('admin')
@Bind(Body())
async create(createCatDto) {
  this.catsService.create(createCatDto);
}

Putting it all together

이제 돌아가서 이것을 RolesGuard와 함께 묶어 봅시다. 현재는 모든 경우에 단순히 true를 반환하여 모든 요청을 진행할 수 있습니다. 현재 사용자에게 할당 된 역할을 처리중인 현재 경로에 필요한 실제 역할과 비교하여 반환 값을 조건부로 만들고 싶습니다. 경로의 역할 (커스텀 메타 데이터)에 액세스하기 위해 프레임 워크에 의해 제공되고@nestjs/core 패키지에서 제공되는 Reflector 헬퍼 클래스를 사용합니다.

@@filename(roles.guard)
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';
import { Reflector } from '@nestjs/core';

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private readonly 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;
    const hasRole = () => user.roles.some((role) => roles.includes(role));
    return user && user.roles && hasRole();
  }
}
@@switch
import { Injectable, Dependencies } from '@nestjs/common';
import { Reflector } from '@nestjs/core';

@Injectable()
@Dependencies(Reflector)
export class RolesGuard {
  constructor(reflector) {
    this.reflector = reflector;
  }

  canActivate(context) {
    const roles = this.reflector.get('roles', context.getHandler());
    if (!roles) {
      return true;
    }
    const request = context.switchToHttp().getRequest();
    const user = request.user;
    const hasRole = () => user.roles.some((role) => roles.includes(role));
    return user && user.roles && hasRole();
  }
}

info 힌트 node.js 세계에서는 권한이 있는 사용자를 request 오브젝트에 첨부하는 것이 일반적입니다. 따라서 위의 샘플 코드에서 request.user에 사용자 인스턴스와 허용된 역할이 포함되어 있다고 가정합니다. 앱에서 아마도 사용자 지정 인증 가드(authentication guard) (또는 미들웨어)에서 해당 연결을 만들 것입니다.

Reflector 클래스를 사용하면 지정된 key에 의해 메타 데이터에 쉽게 액세스할 수 있습니다 (이 경우 키는 roles입니다. roles.decorator.ts파일과 거기에서 만들어진 SetMetadata()호출을 다시 참조하십시오). 위의 예에서 현재 처리 된 요청 메소드의 메타 데이터를 추출하기 위해 context.getHandler()를 전달했습니다. getHandler()는 경로 핸들러 함수에 참조를 제공한다는 것을 기억하십시오.

컨트롤러 메타 데이터를 추출하고 이를 사용하여 현재 사용자 역할을 결정함으로써 이 가드를 보다 일반적으로 만들 수 있습니다. 컨트롤러 메타 데이터를 추출하기 위해 우리는 context.getHandler() 대신 context.getClass()를 전달합니다:

@@filename()
const roles = this.reflector.get<string[]>('roles', context.getClass());
@@switch
const roles = this.reflector.get('roles', context.getClass());

권한이 충분하지 않은 사용자가 엔드 포인트를 요청하면 Nest는 다음 응답을 자동으로 반환합니다.

{
  "statusCode": 403,
  "message": "Forbidden resource"
}

뒤에서 가드가 false를 반환하면 프레임 워크에서 ForbiddenException이 발생합니다. 다른 오류 응답을 리턴하려면 고유한 예외를 처리해야 합니다. 예를 들면 다음과 같습니다.

throw new UnauthorizedException();

가드에 의해 발생 된 예외는 예외 계층(전역 예외 필터 및 현재 컨텍스트에 적용되는 모든 예외 필터)에 의해 처리됩니다.

Last updated