Nest.js Guard 적용하기

2023.05.05

주의! 해당 포스트의 내용은 개인적인 공부 내용으로, 저 만의 방법으로 적용시킨 정상적인 방법이 아닐수도 있습니다.
프로젝트에 적용할 목적이라면 충분히 자료를 찾아보시고 검토를 진행한 다음 적용시키기 바랍니다.


무엇을 할것인가?

Nest.js 에서 JWT 인증환경에 Guard를 적용하여 라우트 보호 하기.


사용환경

Nest.js 백엔드에 사용자 인증은 JWT를 사용하며 graphql 요청을할때 특정 쿼리에는 사용자 인증과 권한을 적용시키기.


조건

  1. graphql의 특정 쿼리와 뮤테이션은 사용자 로그인이 필요.
  2. 사용자 계정은 어드민과 일반 유저로 나뉨.
  3. 어드민일 경우 권한 검증없이 바로 통과, 일반 유저일 경우 추가적인 권한 검증 필요.

개인 프로젝트를 진행하다 기능을 만드는데 위와 같은 조건이 필요했다.
쉽게 요약하자면 일반적인 graphql 쿼리는 누구에게나 공개 되어있지만 개인 정보를 담은 특정 쿼리나 새로운 글을 등록할수있는 mutation은 로그인이 된 사람만 쓸수있도록 제한을 걸어야하며 글 삭제와 같은 본인의 글만 삭제를 할수있어야 할때 삭제하려는 글이 로그인한 본인의 글이 맞는지 검증이 필요하며 어드민이 삭제를 하는거라면 별도의 검증없이 삭제가 가능해야한다.
아직 nest가 익숙하지 않은데 기존 express나 koa에서 쓰던 방식은(REST API 의 경우) 보호 해야할 라우트 앞에 사용자를 검증하는 미들웨어를 붙이는 식으로 사용했다. 그리고 Nest에도 이와 비슷한 미들웨어,가드,인터셉터 등이 있는데 솔직히 다 비슷비슷한 기능과 목적인데 대체 뭔차인지 모르겠다.
아무튼 여기선 guard를 사용하였다.


AuthGuard 구현 (로그인 체크)

guard가 뭔지 모르겠다면 공식문서를 읽어보자. 구현하는 방법까지 나와있다.
명심하자, 프레임워크나 패키지를 가장 정확하게 배울수있는 방법은 공식문서를 읽는것이다.
(물론 전부 영어로 되있고 설명도 불친절한거 읽기 진짜 힘들긴함 ㅎ)

import { JwtService } from '@nestjs/jwt';
import {
  CanActivate,
  ExecutionContext,
  Injectable,
  UnauthorizedException,
} from '@nestjs/common';
import { Request } from 'express';
import { GqlExecutionContext } from '@nestjs/graphql';

@Injectable()
export class AuthGuard implements CanActivate {
  constructor(private jwt: JwtService) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const ctx = GqlExecutionContext.create(context);
    const req = ctx.getContext().req;
    const { access, refresh } = getAccessAndRefresh(req);

    //TODO: rtr 구현 필요
    if (!access && !refresh) {
      throw new UnauthorizedException();
    }

    try {
      const payload = await this.jwt.verifyAsync(access, {
        secret: process.env.JWT_SECRET,
      });
      req.user = payload;
    } catch {
      throw new UnauthorizedException();
    }
    return true;
  }
}

const getAccessAndRefresh = (
  req: Request,
): { access: string; refresh: string } => {
  const { access_token, refresh_token } = req.cookies;
  return { access: access_token, refresh: refresh_token };
};

내가 작성한 Auth guard의 코드이다. 로그인한 사용자만 판단하는 가드인데 graphql 요청에서 쿠키에 들어있는 token으로 로그인한 사용자를 판단한다.
REST api의 가드와는 쪼금 다른부분이 있는데 가드에서 리퀘스트를 읽기 위한 컨텍스트를 얻는 방법이 다르다. 이 문서를 참조해보자.
여기서 jwt 토큰을 검증하고 로그인한 사용자를 판단한뒤 요청한 서비스를 실행시킬지를 결정한다.


DeleteGuard 구현 (본인 소유 체크)

로그인한 사용자를 확인했으니 두번째 조건으로 삭제를 하기전에 사용자 권한을 체크하고 어드민일 경우 true를, 일반 사용자일 경우 삭제하려는게 본인의 것인지 판단하는 가드를 작성한다.

import { CanActivate, ExecutionContext, Injectable, NotFoundException } from '@nestjs/common';
import { Post } from './post.entity';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { GqlExecutionContext } from '@nestjs/graphql';

@Injectable()
export class DeletePostGuard implements CanActivate {
  constructor(
    @InjectRepository(Post)
    private repo: Repository<Post>,
  ) {}
  async canActivate(context: ExecutionContext): Promise<boolean> {
    const ctx = GqlExecutionContext.create(context);
    const req = ctx.getContext().req;
    const args = ctx.getArgs();

    const post = await this.repo.findOneBy({ id: args.id });

    if (!post) {
      throw new NotFoundException();
    }

    //어드민(1)일 경우 true
    if ((req.user.access_level === 1)) {
      return true;
    }

    //어드민이 아닐경우 삭제하려는 post의 소유자가 로그인한 유저인지 확인
    if(post.owner !== req.user.uid){
       return false;
    }
    return true;
  }
}

가드 적용하기

이제 작성한 가드를 실제 처리기에 적용시킨다. 가드를 적용하는 방법은 전체 라우트에 적용시킬수도 있고 특정 메소드에만 적용시킬수도 있다.
여기선 graphql의 resolver 레벨에만 적용시켜 보았다(사실 전역으로 적용시키는법을 아직 모름)

@Resolver('Post')
export class PostResolver {
  constructor(private postService: PostService) {}

  @Mutation()
  @UseGuards(AuthGuard, DeletePostGuard)
  async deletePost(@Args('id') id: string) {
    return this.postService.DeletePost(id);
  }
}

graphql의 resolver 레벨에서 AuthGuard와 DeletePostGuard를 적용시켰다.
UseGuards에 적용한 순서대로 먼저 AuthGuard에서 로그인여부를 먼저판단하고 그 다음으로 삭제하려는 포스트의 소유자가 로그인한 본인인지 확인한다. 만약 로그인하지 않았거나 포스트가 본인 소유가 아니라면 unauthorized 익셉션을 반환한다.

Do you want something exciting?

© 2022. YSH All rights reserved.