Nest

NestJS 토큰 검증 기초 of 기초: Guard와 Passport.js

YATTA! 2024. 12. 13. 21:10

우리는 여러 요청들에 '토큰'을 함께 보내 제가 이 요청을 수행할 수 있는 존재임을 알리게 됩니다. 오늘은 서버의 입장에서 받은 토큰이 정상적인지 여부를 판단하는 과정을 알아보려고 합니다.

 

Nest.js의 토큰 검증 로직에 대한 포스팅은 많지만 조금 더 쉽게 기초와 구현 방법을 풀어낸 포스팅이 있었으면 해서 글을 작성합니다. 

특히 대부분의 포스팅에서 Passport.js 라이브러리를 사용하지만 사용할 때와 사용하지 않을 때의 차이, 즉 왜 사용하는가에 대한 설명이 부족하다고 느꼈고 그런 부분을 알려주는 글이 있다면 처음 Nest.js를 시작하시는 분들께 도움이 될 것이라고 생각하였습니다. 다들 사용하는 라이브러리라도 왜 사용하는지를 정확히 아는 게 저희에겐 중요하니까요.

 

Purporse

1) Guard가 무엇인지 알기.

2) Passport.js를 사용하지 않고 토큰 검증 로직 구현하기.

3) Passport.js가 뭔지, 왜 사용하는지 알기.

 

Guard, 들어보셨나요?

Guard는 CanActivate 인터페이스를 구현하는 @Injectable() 데코레이터가 달린 클래스입니다.

라고 Nest.js 공식문서에서 말하고 있습니다. 저희는 이런 딱딱한 정의가 아니라, Guard를 좀 더 입체적으로 알아보고자 합니다.

 

제 생각에 Guard를 이해할 때 가장 중요한 부분은 Guard가 '역할'이라는 점을 이해하는 것입니다. 

 

Guard는 역할이고, 모든 객체지향에서 그렇듯 역할은 책임을 가집니다. 그렇다면 Guard의 책임은 무엇일까요?

Guard가 가진 가장 중요한 하나의 책임은, '요청을 허용할지 허용하지 않을지 런타임에 결정하는 책임'입니다. 공식 문서에서는 '현재 들어온 요청이 라우트 핸들러에 의해 핸들링 되어야 할지 말지를 특정 조건에 따라서 런타임에 결정하는 책임'이 있다고 말합니다.

 

다시 설명하자면 Guard란 요청의 권한을 판단(=인가)하는 책임이 있는 CanActivate를 구현하는 클래스입니다.

(CanActivate는 @nestjs/common 모듈에 있습니다.)

더보기

Guard를 구현할 때 클래스 이름에 반드시 "Guard"라는 단어가 들어가야 하는 것은 아닙니다. 하지만 Guard라는 이름을 통일성있게 사용하는 것을 권장합니다.

 

코드로 들어가기 전에

저는 서론에 말씀 드린 것처럼 두 가지 방법을 설명드리고자 합니다.

 

1. Passport.js를 쓰지 않고 인증을 구현하는 방법

2. Passport.js를 써서 인증을 구현하는 방법

 

둘 다 간단하니 살펴보시고 원하시는대로 구현하시면 좋을 것 같습니다. ✨

Passport.js는 글이 복잡하게 써진 것 같은데, 이해가 잘 안되는 부분은 질문 주셔도 좋습니다.

자세한 코드는 아래 깃허브 링크를 남겨두었습니다.

 

나의 커스텀 AuthGuard 만들기 (without Passport.js)

저희는 이미 Guard 클래스에 대한 기초 지식이 있습니다. @Injectable()과 CanActivate를 구현해야 한다는 사실을 알고 있죠. 아래는 Nest.js 공식 문서에서 제공하는 Guard 코드입니다.

import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';

@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(
    context: ExecutionContext
  ):Promise<boolean>{
    const request = context.switchToHttp().getRequest();
    return validateRequest(request);
  }
}

저희가 아는 Guard 클래스의 정의에 부합하죠? 그렇다면 우리에게 남은 것은 'validateReuqest(request);' 부분이 됩니다. 즉, 요청으로 들어온 토큰을 검증하는 코드를 작성하기만 하면 되는 거죠. 

 

token을 검증하는 로직은 AuthGuard 클래스 안에 작성하여도 되고, 공통화를 위해 다른 클래스로 빼도 됩니다. 저는 후자의 방식을 선택하였고, Strategy라는 클래스를 사용하여 validate 코드를 따로 작성하였습니다.

@Injectable()
export class JwtStrategy {
  private readonly secret = 'your-secret-key'; // 비밀키 설정 - env를 사용하세요

  validateAccessToken(token: string): AccessPayload {
    try {
      const payload = jwt.verify(token, this.secret) as CustomJwtPayload; // 디코딩된 데이터 반환
      return {
        type: 'access',
        id: payload.id,
        email: payload.email
      };
    } catch (error) {
      console.log(error.json);
      throw new UnauthorizedException();
    }
  }
}

제공해주는 jwt.verify 통하여 토큰을 검증하게 되는데, jwt.verify는 jsonwebtoken 라이브러리에서 제공하는 함수로, JWT 토큰의 유효한지 확인하고 토큰의 Payload를 디코딩하여 반환합니다. 기본 반환값은 string|JwtPayload인데, 저는 CustomJwtPayload를 따로 생성하였습니다. 여러분은 그냥 JwtPayload를 사용해도 괜찮습니다.

export interface CustomJwtPayload {
  id: string;
  email: string;
}

export interface AccessPayload {
  type: 'access';
  id: string;
  email: string;
}

 

놀랍게도 벌써 끝났습니다.

 

이제 토큰 검증이 필요한 API에 토큰 검증이 필요하다고 알려주기만 하면 됩니다. 📢 (Class 또는 Method에 붙여주세요.)

@UseGuards(AuthGuard)

 

토큰 없이 요청을 보낸다면 이제 Unauthorized 에러를 마주할 수 있을 것입니다.

 

Passport.js가... 뭔가요?

Passport is authentication middleware for Node.js. Extremely flexible and modular.

Passport.js는 Node.js.를 위한 인증 미들웨어로, 매우 유연하고 모듈식입니다. 

> 어라... 미들웨어? Guard랑 미들웨어는 다른 거 아니었나요?

더보기

미들웨어와 Guard는 다릅니다. 실행 시간도 다르고 언제 사용하는지도 다른데요. 잠깐 설명을 하자면 미들웨어는 보통 요청을 전역적으로 처리하거나 특정 경로에서만 동작합니다. 메서드나 컨트롤러 수준 세밀한 제어가 어려울 수 있어요. 반면 Guard는 특정 컨트롤러나 메서드 수준에서 요청을 허용하거나 차단할 수 있습니다.

 

그런데 Passport.js는 자신을 미들웨어라고 말해서 헷갈리실 수도 있습니다.

NestJS에서 Guard는 미들웨어 다음 동작하고, Guard에서 사용하는 Passport.js는 미들웨어 방식으로 동작된다고 정리할 수 있을 것 같습니다.

제가 생각할 때 Passport.js 모듈을 사용할 때와 사용하지 않을 때 가장 큰 차이는 검증 로직을 '직접' 구현하지 않아도 된다는 것입니다. 아래 코드를 보시면 감이 오실 거예요. 😎

 

Passport.js 모듈 설치

우선 우리는 필요한 모듈들을 다운로드 받아줘야 합니다.

npm install @nestjs/passport passport passport-jwt

 

세 가지 모듈을 설치하는데, 각각 왜 필요한지에 대한 설명을 정리해봤습니다.

- @nestjs/passport : NestJS와 Passport.js를 연결합니다. PassportModule, AuthGuard 등을 제공합니다.

- passport : Passport.js의 핵심 패키지입니다. @nestjs/passport는 Passport.js 기능을 활용하여서 passport가 설치되지 않으면 작동하지 않습니다.

- passport-jwt : ExtractJwt, Strategy 등을 제공합니다. (필요한 전략에 따라 설치해주시면 됩니다.)

 

저는 @nestjs/passport에서 passport를 사용하는데 왜 함께 설치가 안되고 따로 설치를 해야하는지 궁금했습니다. 그리고 @nestjs/passport의 package.json 파일을 찾아봤는데요. passport가 dependencies가 아니라 peerDependencies로 설치가 되어 있는 걸 발견했습니다. 

@nestjs/passport 모듈

peerDependencies는 함께 설치되지 않고 사용자가 직접 설치하도록 열어둔 부분인데요. 아마 호환성 등 문제를 고려해서 그렇게 해둔게 아닐까 싶습니다.

 

Passport.js로 인증 구현해보기

이제 필요한 모듈도 설치를 했으니, 본격적으로 인증에 들어가볼까요?

import { Module } from '@nestjs/common';
import { PassportModule } from '@nestjs/passport';

@Module({
  imports: [PassportModule],
  providers: [],
  exports: []
})
export class JwtModule {}

우선 PassportModule을 import 해주어야 합니다.

 

그 다음으로는 Strategy 클래스를 먼저 구현해주겠습니다.

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      secretOrKey: 'test' // 시크릿 키는 따로 관리해주세요
    });
  }

  async validate(payload: any) {
    return { userId: payload.sub, username: payload.username };
  }
}

validate 함수를 보시면, 기존과는 다르게 return 부분만 작성되어있는걸 보실 수 있습니다.

validate 함수는 Passport의 검증 로직 이후 호출됩니다. 검증이 완료되고 정상적인 토큰이라고 판단이 되면 validate 메서드가 실행됩니다. 보이시는 것처럼 필요한 정보를 응답해주거나, 사용자 정보를 조정하는 추가 작업을 수행할 수 있습니다.

 

PassportStrategy의 첫 번째 파라미터는 전략을 선택하는 부분입니다. 처음에 passport-jwt 모듈을 설치했죠? 해당 모듈에서 제공해주는 전략을 사용합니다. (Strategy)

- 전략이란 간단하게 말하자면 사용할 방법, 여기서는 토큰 검증에 사용할 방식이라고 이해해주시면 될것같습니다.

두 번째 파라미터는 전략의 이름을 설정해주는 부분입니다. 필수값은 아니지만 설정해주는 게 좋은 것 같습니다. 특히 여러개의 Strategy를 사용할때는 꼭 작성해주세요.

 

Guard를 사용하는 법은 두 가지가 있는데요. 첫 번째는 제공해주는 Guard를 그대로 사용하는 것입니다.

@UseGuards(AuthGuard('jwt'))

아까처럼 Guard 클래스 생성도 필요가 없고, AuthGuard를 사용해 작성해주면 'jwt' 전략을 사용해서 인증 처리를 해줄 수 있습니다.

 

직접 명시적으로 클래스를 생성하는 방법도 있습니다. 저는 이 방법을 선택했어요. 

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}

AuthGuard의 파라미터로 어떤 네이밍의 전략을 사용하는지를 넘겨줍니다. Guard인데 CanActivite를 구현하지 않는 이유는 AuthGuard가 CanActivate를 구현한 클래스이기 때문입니다.

 

그러면 UseGuards는 아래와 같이 적용시킬 수 있습니다.

@UseGuards(JwtAuthGuard)

 

이처럼 Passport.js를 사용하면 이렇게 Guards를 간단히 구현할 수 있고, 네이밍으로 어떤 전략을 사용할 것인지 쉽게 선택이 가능합니다.

 

Access Token과 Refresh Token의 인증 방식이 다르거나, OAuth와 자체 인증 방식을 둘 다 구현하는 경우 각각에 맞는 Guard와 Strategy가 필요할것입니다. 이 때 Passport.js를 사용한다면 확장성 측면에서 많은 이득을 볼 수 있겠죠?! 특히 복잡한 애플리케이션에서 잘 쓰일 것 같습니다.

 

마무리하며

오늘은 NestJS로 인증을 구현하는 방법을 알아보고, Passport.js 라이브러리에 대해서도 알아보았습니다.

헷갈리거나 피드백 주실 부분 있으시다면 말씀 부탁드립니다. 끝까지 읽어주셔서 감사합니다. ☺️

 

코드를 참고하고 싶다면 해당 레포지토리를 확인해주세요. 

스타는 항상 감사합니다. ⭐️

 

GitHub - jongeuni/nestjs-auth: Very easy and key certification service samples. Nest.js + Mongoose +JWT.

Very easy and key certification service samples. Nest.js + Mongoose +JWT. - jongeuni/nestjs-auth

github.com

'Nest' 카테고리의 다른 글

NestJs에서 간단하게 엑셀 다운로드 시키는 방법  (0) 2024.04.15