본문 바로가기
Nestjs

[NestJs] Guards 알아보기

by 동복이 2024. 3. 13.

Guard 란?

Nestjs에서 Guard는 인증과 권한 부여와 같은 보안 관련 로직을 처리하는 데 사용됩니다.

특정 조건이 충족될 때까지 해당 요청이 핸들러로 전달되지 않도록 역할을 하기 때문에 요청을 처리하기 전 사용자의 인증 상태나 권한을 검증할 수 있습니다.

 

Guards의 역할

사용자의 권한이 user인지 admin인지 검증하거나 토큰을 검증하는 등 인증 및 권한 검사에 사용됩니다.

Client에서 Controller까지 요청이 도달하기 전 중간 단계에서 동작하며 요청을 수락하거나 거부할 수 있는 논리적 조건을 정의 할 수 있습니다.

 

Guard 파일 생성

src
 ㄴ guards
     ㄴ auth.guard.ts
     ㄴ roles.decoretor.ts
     ㄴ roles.guard.ts

 

Guard 사용 방법

Guard는 pipes와 filter와 달리 내장 메소드가 존재하지 않기 때문에 class를 별도로 정의 해줘야합니다.

canActivate 함수는 현재 요청이 특정 조건을 만족하는지 판단하여 진행 여부를 결정하는 함수입니다.

canActivate를 사용하여 인터페이스를 구현하며 true를 반환하면 요청에 대한 처리가 계속되고 false를 반환하면 요청이 중단됩니다.

이번 시간에는 기본적인 문법과 더미 객체를 사용하여 guard가 어떤식으로 동작하는지에 대하여 알아보겠습니다.

 

1. Authorization guard

인증부분을 처리하는 guard 입니다.

client에서 요청을 보내면 Header에 포함된 access token을 받아와 검증합니다.

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

@Injectable()
export class AuthGuard implements CanActivate {
  // canActivate 는 Nestjs의 guard에서 필수적으로 구현해야 하는 메소드
  // 현재 요청이 계속 진행 될 수 있는지 여부를 반환
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    // ExecutionContext를 통해 현재 요청에 대한 정보를 가져옴
    const request = context.switchToHttp().getRequest();
    
    // 요청 헤더에서 토큰을 가져옴
    // const token = request.headers['authorization'].split(' ')[1];

    // 현재는 테스트를 위해 더미 토큰을 사용
    const token = 'isToken';

    // this.validateToken(token) 메소드를 통해 토큰을 검증하고 결과를 반환해야하지만 현재는 간단한 테스트를 위해 더미 객체를 사용
    request.user = { user_id: 1, username: 'test', roles: ['user'] };    


    // 가져온 토큰을 검증
    return this.validateToken(token);
  }

  // 지금은 간단히 토큰의 유무만 판단하지만 보통 jwt를 활용하여 토큰을 검증함
  private validateToken(token: string): boolean {
    if(!token) return false;
    try {
      // 토큰 검증 로직
      // 토큰의 서명이 유효하고 만료되지 않았다면 true 반환
      return true;
    } catch (error) {
      // 토큰 검증 중 오류가 발생한 경우 false 반환
      return false;
    }
  }
}

 

Method Scope에서 UseGuard(AuthGuard) 적용

// app.controller.ts
import { Controller, Get, HttpException, Param, ParseIntPipe, Post, SetMetadata, UseFilters, UseGuards, UsePipes } from '@nestjs/common';
import { AppService } from './app.service';
import { HttpExceptionFilter } from './filter/http-exception.filter';
import { AuthGuard } from './guards/auth.guard';
import { RolesGuard } from './guards/roles.guard';
import { Roles } from './guards/roles.decorator';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get('/auth-guard-test')
  @UseGuards(AuthGuard)
  authGuardTest(
    // request 객체를 가져오기 위해 @Req() 데코레이터 사용
    // AuthGuard를 통과하게 된 결과를 가져오기 위해 사용
    @Req() request: any 
  ) {
    // request 객체에 user 객체를 추가했기 때문에 사용 가능
    const user = request.user;
    return user;
  }
}

// 결과
/*
    {
      "user_id": 1,
      "username": "test",
      "roles": [
        "user"
      ]
    }
*/

 

2. Role-based authentication

사용자 권한 관련 guard로 권한이 user인지 admin인지에 대한 검사를 하는 로직입니다.

사용자 권한의 경우 RolesGuard를 단독으로 쓰는거보다 AuthGuard와 함께 사용하여 인증과 권한을 관리하는 일반적인 방법입니다.

이 또한 간단한 더미 객체를 사용하여 테스트해보겠습니다.

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

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    // Roles 데코레이터를 사용해 역할을 가져옴
    const roles = this.reflector.get<string[]>('roles', context.getHandler());

    // 설정된 역할이 없는 경우, 모든 사용자가 접근할 수 있도록 true를 반환
    if (!roles) return true;

    // ExecutionContext를 통해 현재 HTTP 요청 객체를 가져옴
    // const request = context.switchToHttp().getRequest();
    // const user = request.user;


    // 현재는 간단한 테스트를 위해 더미 객체를 사용
    const user = { user_id: 1, username: 'test', roles: ['user'] };


    // 사용자가 요청한 역할이 필요한 역할 중 하나인지 확인
    return roles.some((role) => user.roles?.includes(role));
  }
}

 

// roles.guard.ts
import { SetMetadata } from "@nestjs/common";

// Roles 데코레이터는 특정 메소드나 클래스에 역할 기반 접근 제어를 적용하기 위해 사용
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);

 

Method Scope에서 UseGuard(AuthGuard, RoleGuard) 적용

import { Controller, Get, HttpException, Param, ParseIntPipe, Post, Req, SetMetadata, UseFilters, UseGuards, UsePipes } from '@nestjs/common';
import { AppService } from './app.service';
import { HttpExceptionFilter } from './filter/http-exception.filter';
import { AuthGuard } from './guards/auth.guard';
import { RolesGuard } from './guards/roles.guard';
import { Roles } from './guards/roles.decorator';
import { request } from 'http';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get('/role-guard-test')
  @UseGuards(AuthGuard, RolesGuard)
  @Roles('user') // 어떤 역할이 접근 가능한지 설정
  roleGuardTest(
    // request 객체를 가져오기 위해 @Req() 데코레이터 사용
    // RolesGuard를 통과하게 된 결과를 가져오기 위해 사용
    @Req() request: any
  ) {
    const user = request.user;
    return user;
  }
}

// 결과
// const user = { user_id: 1, username: 'test', roles: ['admin'] };
// RoleGuard에서 생성된 user의 정보 중 roles가 "user" 일 경우
{
  "user_id": 1,
  "username": "test",
  "roles": [
    "user"
  ]
}

// roles가 "admin" 일 경우
{
  "message": "Forbidden resource",
  "error": "Forbidden",
  "statusCode": 403
}

 

 

항상 프로젝트를 할때 로그인 관련 인증 부분에 대한 고민을 많이 하였습니다.

그렇기에 이런식의 관리 방법이 가독성 및 유지보수 하기에도 훨씬 편한거 같고 어느 부분이 어떤 인증과 검증을 사용하는지에 대한

구조가 명확하다는 점이 마음에 들었습니다.

728x90

'Nestjs' 카테고리의 다른 글

[NestJs] Middleware 알아보기  (1) 2024.03.15
[NestJs] Interceptors 알아보기  (0) 2024.03.14
[NestJs] Pipes 알아보기  (1) 2024.03.12
[NestJs] Exception filters 알아보기  (0) 2024.03.11
[NestJs] 기본 구조 살펴보기  (0) 2024.03.10