Loading...
본문 바로가기
👥
총 방문자
📖
0개 이상
총 포스팅
🧑
오늘 방문자 수
📅
0일째
블로그 운영

여러분의 방문을 환영해요! 🎉

다양한 개발 지식을 쉽고 재미있게 알려드리는 블로그가 될게요. 함께 성장해요! 😊

코딩 정보/NestJs

[NestJs] 토큰 가드를 구현해보자

by 꽁이꽁설꽁돌 2025. 1. 24.
728x90
반응형
     

목차

     

    이번에는 저번에 구현했던 토큰에 대해 가드를 구현하고자 한다.

    https://be-senior-developer.tistory.com/279

     

    [NestJs] 로그인 기능을 구현해 보자

    목차   대략적인 로그인 구현 흐름 정리 /** * 만드려는 기능 * * 1) registerWithEmail * - email, nickname, password를 입력받고 사용자를 생성한다. * - 생성이 완료되면 accessToken과 refreshToken을 반환한다. * -

    be-senior-developer.tistory.com

     

    NestJs의 요청 라이프 사이클

     

     

    여기서 질문! 토큰을 파이프로 처리하면 안되는 걸까?

    파이프와 가드의 역할 차이
    • 파이프(Pipe):
      주로 데이터 변환 및 유효성 검사를 수행합니다. 클라이언트 요청의 데이터를 처리하기 위해 사용됩니다. 데이터의 형태를 보장하거나, 특정 조건에 맞는 데이터를 필터링합니다.
    • 가드(Guard):
      액세스 제어를 담당합니다. 요청이 처리되기 전에 특정 조건(예: 인증, 권한)이 충족되는지 확인하고, 조건을 충족하지 않으면 요청을 막습니다.

    토큰을 파이프로 검증하면 안 되는 이유

    1. 토큰은 인증 및 권한 제어의 핵심 요소임
      토큰은 사용자 인증(누구인지 확인)과 권한(무엇을 할 수 있는지 결정)을 위한 중요한 요소입니다. 인증 및 권한 처리는 액세스 제어의 핵심 작업이므로, 파이프 대신 가드에서 처리하는 것이 맞습니다.
    2. 파이프는 데이터를 변환하거나 형식을 검증하는 데 적합
      파이프는 단순한 데이터 유효성 검사 및 변환을 위한 도구입니다. 토큰의 복호화나 유효성 검증은 단순 데이터 확인 이상의 작업(예: 서명 검증, 유효기간 확인 등)이 필요하므로, 파이프로 처리하기에는 부적합합니다.
    3. 가드는 요청을 아예 차단할 수 있음
      가드는 조건을 충족하지 않을 경우 요청을 애초에 처리하지 않고, 서버에서 즉시 거부합니다. 반면, 파이프는 데이터를 변환하거나 검증한 후 다음 단계로 데이터를 전달하므로, 부적합한 토큰을 처리하려는 위험이 있습니다.
    4. 토큰 검증은 보안상 중요한 작업임
      토큰 검증은 민감한 보안 작업으로, 전문적으로 설계된 인증 라이브러리나 가드(예: AuthGuard)를 사용하는 것이 더 안전합니다. 파이프로 검증하면 실수로 토큰 처리 로직을 누락하거나 잘못된 상태로 데이터를 전달할 위험이 있습니다.

     

    따라서 우리는 여기서 guard를 구현해 볼 것이다.
    가드역할은 다음과 같다.

     

     

    우리는 여기서 authorization을 넣지 않았을 때 함수가 실행되게 하고 싶지 않다.

    하지만 다음을 보면 함수가 실행된 것을 볼 수 있다.

     

    Basic Token 가드 구현하기

    /**
     * 구현할 기능
     *
     * 1) 요청객체 (request)를 불러오고
     *  authorization header로부터 토큰을 가져온다.
     * 2) authService.extractTokenFromHeader를 이용해서
     *    사용할 수 있는 형태의 토큰을 추출한다.
     * 3) authService.decodeBasicToken을 실행해서
     *    email과 password를 추출한다.
     * 4) email과 password를 이용해서 사용자를 가져온다.
     *    authService.authenticatedWithEmailAndPassword
     * 5) 찾아낸 사용자를 (1) 요청 객체에 붙여준다.
     *    req.user = user;
     */
    
    import {
      CanActivate,
      ExecutionContext,
      Injectable,
      UnauthorizedException,
    } from '@nestjs/common';
    import { AuthService } from '../auth.service';
    
    @Injectable()
    export class BasicTokenGuard implements CanActivate {
      constructor(private readonly authService: AuthService) {}
    
      async canActivate(context: ExecutionContext): Promise<boolean> {
        const req = context.switchToHttp().getRequest();
        // {authorization: 'Basic fsfsfsfsfsf'}
        //sfsfsfsfsfsf
        const rawToken = req.headers.authorization;
        if (!rawToken) {
          throw new UnauthorizedException('토큰이 없습니다!');
        }
        const token = this.authService.extractTokenFromHeader(rawToken, false);
        const { email, password } = this.authService.decodeBasicToken(token);
    
        const user = await this.authService.authenticatedWithEmailAndPassword({
          email,
          password,
        });
        req.user = user;
        return true;
      }
    }

     

     

    코드 작성 전

     

     

    다음과 같이 이제는 토큰이 없을 시 오류 메시지가 나오고 함수가 아예 실행되지 않는다.

     

     

    Bearer 토큰 가드 구현하기

    /**
     * 구현할 기능
     *
     * 1) 요청객체 (request)를 불러오고
     *  authorization header로부터 토큰을 가져온다.
     * 2) authService.extractTokenFromHeader를 이용해서
     *    사용할 수 있는 형태의 토큰을 추출한다.
     * 3) authService.decodeBasicToken을 실행해서
     *    email과 password를 추출한다.
     * 4) email과 password를 이용해서 사용자를 가져온다.
     *    authService.authenticatedWithEmailAndPassword
     * 5) 찾아낸 사용자를 (1) 요청 객체에 붙여준다.
     *    req.user = user;
     */
    
    import {
      CanActivate,
      ExecutionContext,
      Injectable,
      UnauthorizedException,
    } from '@nestjs/common';
    import { AuthService } from '../auth.service';
    import { UsersService } from 'src/users/users.service';
    
    @Injectable()
    export class BearerTokenGuard implements CanActivate {
      constructor(
        private readonly authService: AuthService,
        private readonly userService: UsersService,
      ) {}
    
      async canActivate(context: ExecutionContext): Promise<boolean> {
        const req = context.switchToHttp().getRequest();
        // {authorization: 'Basic fsfsfsfsfsf'}
        //sfsfsfsfsfsf
        const rawToken = req.headers.authorization;
        if (!rawToken) {
          throw new UnauthorizedException('토큰이 없습니다!');
        }
        const token = this.authService.extractTokenFromHeader(rawToken, true);
        const result = await this.authService.verifyToken(token);
    
        /**
         * request에 넣을 정보
         *
         * 1) 사용자 정보
         * 2) token -  token
         * 3) tokenType - access | refresh
         *
         */
    
        const user = await this.userService.getUserByEmail(result.email);
        req.user = user;
        req.token = token;
        req.tokenType = result.type;
        return true;
      }
    }
    
    @Injectable()
    export class AccessTokenGuard extends BearerTokenGuard {
      async canActivate(context: ExecutionContext): Promise<boolean> {
        await super.canActivate(context);
    
        const req = context.switchToHttp().getRequest();
    
        if (req.tokenType !== 'access') {
          throw new UnauthorizedException('Access Token이 아닙니다.');
        }
        return true;
      }
    }
    
    @Injectable()
    export class RefreshTokenGuard extends BearerTokenGuard {
      async canActivate(context: ExecutionContext): Promise<boolean> {
        await super.canActivate(context);
    
        const req = context.switchToHttp().getRequest();
    
        if (req.tokenType !== 'refresh') {
          throw new UnauthorizedException('Refresh Token이 아닙니다.');
        }
        return true;
      }
    }

     

     

    커스텀 데코레이터 만들기

    우리는 토큰 가드를 통과할 때 인자를 쉽게 반환하여 쓰고 싶다. 

    그럴때 커스텀 데코레이터를 사용해서 원하는 값을 추출해 주면 된다.

     

    user.decorator.ts

    import {
      createParamDecorator,
      ExecutionContext,
      InternalServerErrorException,
    } from '@nestjs/common';
    
    export const User = createParamDecorator((data, context: ExecutionContext) => {
      const req = context.switchToHttp().getRequest();
    
      const user = req.user;
      if (!user) {
        throw new InternalServerErrorException(
          'User 데코레이터는 AccessTokenGuard와 함께 사용해야 합니다. Request에 user 프로퍼티가 존재하지 않습니다!',
        );
      }
      return user;
    });

     

     

    이렇게 만든 토큰 가드들을 이제 사용해야 한다.

     

    posts.controller.ts

      @Post()
      @UseGuards(AccessTokenGuard)
      postPosts(
        @User() user: UsersModel,
        @Body('title') title: string,
        @Body('content') content: string,
      ) {
        const authorId = user.id;
        return this.postsService.createPost(authorId, title, content);
      }

     

    반응형