본문으로 바로가기

Guard + MetaData

category Node, Nest, Deno/🦁 Nest.js 2020. 11. 24. 20:49

docs.nestjs.com/guards#guards

 

Documentation | NestJS - A progressive Node.js framework

Nest is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, is built with TypeScript and combines elements of OOP (Object Oriented Progamming), FP (Functional Programming), and FRP (Functional Reac

docs.nestjs.com

Guard에는 일반 http 통신 가드, ws 통신가드가 있습니다만 본질적인 차이는 없다고 하네요.

There is no fundamental difference between web sockets guards and regular HTTP application guards. The only difference is that instead of throwing HttpException, you should use WsException.

 

 

미들웨어로 특정 요청을 막으면 될텐데 왜 굳이 Guard를 써야할까요? 공식 문서의 답은 다음과 같습니다.

 

But middleware, by its nature, is dumb. It doesn't know which handler will be executed after calling the next() function. On the other hand, Guards have access to the ExecutionContext instance, and thus know exactly what's going to be executed next. They're designed, much like exception filters, pipes, and interceptors, to let you interpose processing logic at exactly the right point in the request/response cycle, and to do so declaratively. This helps keep your code DRY and declarative.

 

HINT : Guards are executed after each middleware, but before any interceptor or pipe.

 

Guard 정의

이론은 여기까지하고 구체적으로 Guard를 만들고 사용해보겠습니다.

A guard is a class annotated with @Injectable() decorator. Guards should implement the CanActivate interface.

 

CanActivate interface는 다음과 같이 생겼습니다. 

export interface CanActivate {
    canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean>;
}

 

다음과 같이 guard를 정의해줄 수 있습니다. false를 반환하면 막고, true를 반환하면 넘어갑니다.

ExecutionContext는 다음 공식 문서를 참고합시다. docs.nestjs.com/fundamentals/execution-context

@Injectable()
export class AuthGaurd implements CanActivate {
  canActivate(context: ExecutionContext) {
    console.log(context);
    return false; // block. Forbidden resource error message
  }
}

 

ExecutionContext는 기본적으로 http로 작동하는 REST 기반 context를 반환합니다. gql 기반 context를 사용하시려면(apollo-server를 통해 만들어준 context) 다음과 같은 변환 과정을 거쳐야 합니다. 자세한 내용은 다음 공식 문서 참조합시다. Next가 말하기로는 Next의 가장 강력한 기술 중 하나가 execution context라고 하네요. 알아둡시다.

docs.nestjs.com/graphql/other-features#execution-context

@Injectable()
export class AuthGaurd implements CanActivate {
  canActivate(context: ExecutionContext) {
    const gqlContext = GqlExecutionContext.create(context).getContext();
    console.log(gqlContext);
    return true; // always pass
  }
}

 

 

@UseGuards로 Guard 사용하기

 

controller나 resolver 단에서 @UseGuards 데코데이터를 이용해 해당 guard를 사용할 수 있습니다.

resolver에서는 다음과 같이 사용하고

@Query(() => User)
@UseGuards(AuthGaurd)
me(@Context() context) {
  if (!context.user) {
    return;
  } else {
    console.log(context.user);
    return context.user;
  }
}

controller에는 다음과 같이 사용합니다.

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

 

 

전역에서 Guard를 사용하고 싶다면?

 

매번 Guard Decorator에 올리기보다 자동으로, 모든 핸들러에 Guard를 적용하고 싶다면 아래와 같이 사용합니다.

 

import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core'; // nest가 제공하는 상수임
import { AuthGaurd } from './auth.guard'; // 코더가 만든 guard

@Module({
  providers: [{ provide: APP_GUARD, useClass: AuthGaurd }],
})
export class AuthModule {}
@Module({
  .. 중략
  imports: [
    AuthModule, // 앱 모듈에 넣어주기
    RestaurantsModule,
  ],
  controllers: [],
  providers: [],
})
export class AppModule implements NestModule {
  ... 중략
}

 

 

Set Metadata for roles per handler

docs.nestjs.com/guards#setting-roles-per-handler

 

Cineps 만들때도 그랬지만, 유저에게는 admin 유저가 있고, 일반 유저가 있고, 혹은 레벨 시스템을 도입한다면 레벨이 있습니다. 때문에 특정 요청에 해당 유저의 권한 레벨이 어느 정도인지 체크할 필요가 있습니다. 이럴 때에 Guard를 사용하면 딱이지만, MetaData를 활용하면 좀 더 우아하게 처리할 수 있습니다.

 

메타 데이터가 무엇인가, Guard에 대한 문서를 보면 다음과 같습니다.

Nest provides the ability to attach custom metadata to route handlers through the @SetMetadata() decorator.

그 '커스텀 메타데이터'가 무엇인가 하면 아래 공식 문서를 참고하면 이해할 수 있습니다.

 

docs.nestjs.com/fundamentals/execution-context#reflection-and-metadata

Nest provides the ability to attach custom metadata to route handlers through the @SetMetadata() decorator. We can then access this metadata from within our class to make certain decisions.

With the construction, we attached the roles metadata (roles is a metadata key and ['admin'] is the associated value) to the create() method.

@Post()
@SetMetadata('roles', ['admin']) // 단순히 그냥 key/value 값일 뿐임
async create(@Body() createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);
}

 

일단 이렇게 사용해도 되지만, 직접 Router에 (graphql은 Resolver에) @SetMetadata를 사용하는 것은 좋지 않고, 데코레이터를 만들어서 활용하는 것이 좋다고 합니다.

While this works, it's not good practice to use @SetMetadata() directly in your routes. Instead, create your own decorators, as shown below:

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

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

 

controller에 사용

@Post()
@Roles('admin')
async create(@Body() createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);
}

 

resolver에 사용

@Query(() => User)
@Roles(['Owner']) // @role이 있으면 권한을 체크한다는 의미임
me(@AuthUser() authUser: User) {
  return authUser;
}

 

 

 

metadata를 guard와 함께 사용하자

위와 같이 설정한 메타데이터는 그 자체로는 아무 의미가 없습니다. 

우선 앞서 특정 Guard가 전역에서 사용되는 것을 전제하고 metadata를 활용해보겠습니다.

 

Reflector를 이용하여 메타데이터를 가져올 수 있습니다. Retrieve metadata for a specified key for a specified target.

const roles = this.reflector.get<AllowedRoles>('roles', context.getHandler());
@Injectable()
export class AuthGaurd implements CanActivate {
  constructor(private readonly reflector: Reflector) {} // reflector를 이용해 metadata 가져옴

  canActivate(context: ExecutionContext) {
    const roles = this.reflector.get<AllowedRoles>('roles', context.getHandler());

    // role이 없다는 건 권한, 로그인 여부 상관 없이 public하게 동작하는 handler라는 것
    if (!roles) {
      return true; // pass
    }
    const gqlContext = GqlExecutionContext.create(context).getContext(); // apollo-server context를 받기 위한 과정
    const user: User = gqlContext.user;
    if (!user) {
      return false; // 메타 데이터가 있는데 찾는 유저 정보가 없다? block
    }
    if (roles.includes('Any')) {
      return true; // Any라면 로그인 되기만 하면 통과되는 것으로.
    }
    return roles.includes(user.role); // pass
  }
}

 

이제 아래와 같이 Any를 넘겨주면 AuthGuard를 통해 통과하면서 유저의 로그인 여부와 권한 체크를 하게되겠죠?

@Query(() => User)
@Role(['Any']) // @role이 있으면 권한을 체크한다는 의미임
me(@AuthUser() authUser: User) {
  return authUser;
}

darren, dev blog
블로그 이미지 DarrenKwonDev 님의 블로그
VISITOR 오늘 / 전체