ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Authentication, Nest.js, JWT] Nest.js에서 JWT 동적모듈,미들웨어 구현해보기
    Back-end 2021. 12. 12. 21:40

     

     

    안녕하세요! 오늘은 더 나은 사람들 서비스의 로그인 부분 진행 중 nest.js 환경에서 JWT를 이용해 로그인을 구현하고 있었습니다. 🐤

    다들 로그인 인증 부분을 어떻게 처리하시나요? 👀

    nest.js의 강력한 인증라이브러리인 passport가 있지만, JWT 동적 모듈을 따로 구현하여 컨셉도 다시 익히고 개념 정리도 하면서 포스팅을 할려고 합니다. 

     

    구현 방향은 다음과 같습니다.

     

    프로젝트 내 구현 프로세스

    1. 로그인 시 JwtService의 signToken 함수로 praviate key와 함께 토큰을 사인 (JSON 안의 내용은 다음과 같습니다.)

    const token = this.jwtService.signToken({
            id: (await user).id,
            nickname: (await user).nickname,
            check: 'copyright of better-man Inc.',
          });

    2. 다음의 정보는 request-header의 'x-jwt'에 저장됩니다.

     

    3. JWT 미들웨어에서 'x-jwt'의 request-header를 찾아내고, JwtService의 verifyToken으로 해당 토큰을 디코딩합니다.

     

    4. 디코딩된 내용 중 JSON에 들어있는 user.id를 활용하여, user resolver에서 id로 User를 찾아냅니다.

     

    5. 찾아낸 유저를 'user' request-header에 담아 보냅니다.

     

    6.graphql Context를 이용하여 user를 header에 context처리 합니다.

     

     

    What is the JsonWebToken?

    jsonwebtoken은 무엇일까요? 그리고 왜 사용할까요? 우리가 사용자를 인증할 때 사용하는 방법 중 하나로, 유저에게 토큰을 발급해주고, 그 토큰을 인증하여 유저를 로그인 시키는 방식입니다. 그 토큰은 제작자의 시크릿 코드에 의해 알고리즘화 된 인증가능 토큰입니다. 

     

    JWT는 속성 정보를 JSON 데이터 구조로 표현한 토큰으로 RFC7519 표준 입니다.  JSON 전자서명을 URL-safe 문자열에 포함시키는 것이지요. 이 시크릿 코드는 제작자의 자유입니다만, 강력한 솔팅을 위해 랜덤 키 젠등의 사이트를 통해 시크릿 코드를 정하도록 합니다.

    JWT의 URL-safe 문자열은 쉽게 디코딩이 가능합니다. 즉, 우리는 token안에 민감한 정보를 넣는 일이 없어야 합니다.

     

    JWT 토큰으로 유저를 인증하는 프로세스는 다음과 같습니다.

    jwt workflow

    다음과 같은 프로세스로 우리는 유저를 인증할 수 있습니다.

     

    JWT with Nest.js

    우선 우리는 jwt를 사용하기 위해 jsonwebtoken 패키지를 다운로드 받아야합니다.

    npm install jsonwebtoken @types/jsonwebtoken

    다운로드가 끝났으면 우선 우리가 해야할 일을 살펴봅시다!

     

    첫번 째 프로세스에서 우리는 클라이언트 측에서 로그인시 json.sign을 한 토큰을 서버측에 넘겨주어야 합니다. 논리적으로 생각을 해 보았을 때 로그인 로직에서 우리는 jwt를 사인하여 넘겨주는 코드를 작성해야 합니다. 그렇다면, 토큰을 sign할 때 필요한게 뭘까요?! 아주 중요한 'Private key' 입니다!

     

    이 Private key는 우리가 발급한 토큰이라는 것을 인증해 줄 것입니다. 토큰의 내용물이 강제로 변경이 될 경우 우리는 이 private key로 우리의 사이트에서 발급된 토큰인지 아닌지를 확인할 수 있습니다. 이 키는 자신이 정하면 되고, 저는 랜덤키젠 같은 사이트를 이용하여 사용하였습니다.

     

    generate Dynamic JwtModule

    다음으로는 jwt 처리를 할 모듈을 생성하여 app.module.ts에 inject하도록 하겠습니다.

    @Global()
    @Module({})
    export class JwtModule {
      static forRoot(): DynamicModule {
        return {
          module: JwtModule,
        };
      }
    }

    우선 다음과 같이 모듈을 생성합니다. 혹시 generate module 등 프로젝트 구성을 하는 법이 궁금하시다면 다음 포스트를 참고해주세요.

    https://sangjuntech.tistory.com/5

     

    [Nest.js, Graphql] 간단하게 Nest.js와 Graphql 기반의 프로젝트 구성하는 법

    안녕하세요. 이 포스트는 node.js위에 가동하는 프레임워크인 nest.js와 Graphql 기반의 프로젝트 초기 구성하는 법을 포스팅해보겠습니다.  Nest.js? Nest(NestJS)는 효율적이고 확장 가능한 node.js 서버 측

    sangjuntech.tistory.com

     

    우리는 option값을 가지는 동적 모듈이 필요합니다. 따라서 컨퍼런스에 따른 forRoot 다이나믹 모듈을 만들어주도록 합니다. 그 option은 privateKey가 될 것입니다. jwtModule은 jwt의 사인, 디코딩등을 담당하기 때문에 무조건 필요한 옵션이죠.

     

    jwt의 옵션에 대한 인터페이스를 하나 만들어 보겠습니다.

    export interface JwtModuleOptions {
        privateKey: string;
    }

     

    private키를 받을 옵션 인터페이스를 만들었으니, 우리는 module에 이에 대한 옵션값을 모듈에 제공할 필요가 있습니다. 그래야 우리는 app.module.ts로부터 환경변수에 있는 privatekey를 jwt모듈에 끌고올 수 있을 테니까요!

    import { DynamicModule, Global, Module } from '@nestjs/common';
    import { JwtModuleOptions } from './interfaces/jwt-module-options.interface';
    import { CONFIG_OPTIONS } from './jwt-constant';
    import { JwtService } from './jwt.service';
    
    @Global()
    @Module({})
    export class JwtModule {
      static forRoot(options: JwtModuleOptions): DynamicModule {
        return {
          module: JwtModule,
          providers: [
            {
              provide: CONFIG_OPTIONS,
              useValue: options,
            },
            JwtService,
          ],
          exports: [JwtService],
        };
      }
    }

    이제 모듈에 대한 세팅은 끝났으니, 토큰의 인증과 사인에 대한 로직을 service.ts에 작성을 해보도록 합니다.

     

    import { Inject, Injectable } from '@nestjs/common';
    import { JwtModuleOptions } from './interfaces/jwt-module-options.interface';
    import { CONFIG_OPTIONS } from './jwt-constant';
    import * as jwt from 'jsonwebtoken'
    
    @Injectable()
    export class JwtService {
        constructor(@Inject(CONFIG_OPTIONS) private readonly options: JwtModuleOptions) {}
        signToken(payload: object): string {
            return jwt.sign(payload, this.options.privateKey)
        }
        verifyToken(token: string) {
            return jwt.verify(token, this.options.privateKey)
        }

    jwt.service.ts에서 우리는 signToken이란 함수와 verifyToken이라는 함수를 만듭니다. signToken은 클라이언트 측에서 사용될 것이고, verifyToken은 다시 우리가 만들게 될 middleware에서 사용될 것입니다.

     

    우리는 app.module.ts에서 선언된 jwtmodule의 옵션값으로 받은 privateKey를 암호화키로 사용하여 해당 토큰을 발급하게 될 것입니다. 모듈을 다 작성했으니, app.module.ts에 우리의 jwt.module을 제공하러 갑시다!

     

     JwtModule.forRoot({
          privateKey: process.env.TOKEN_SECRET,
        }),

    app.module.ts에 우리의 jwtModule을 제공하고, 환경변수에 소중하게 적어놓은 비밀키를 사용하도록 합니다 ㅎㅎ. 자 이제 필요한 준비는 거의 다 되었습니다! 이제 가장 중요한 인증처리를 위한 중간다리! 미들웨어를 만들어야 합니다!

     

    이제 그럼 다시 프로세스로 올라가서 우리가 가장 먼저 해야할 일인 login mutation에서 토큰을 사인하는 코드를 작성하도록 합니다. 작성이 완료된 코드는 'x-jwt'로 http header에 저장이 될 것입니다. 우리는 이 것을 가져와 처리를 하는 미들웨어를 만들어보도록 합시다!

     

    Middleware

    nest.js의 미들웨어는 express환경에서와 동일합니다 use, req, res, next를 통해 우리의 토큰을 인증하고 헤더를 request할 것입니다. 

     

    우선 코드를 먼저 살펴봅시다.

    import { Injectable, NestMiddleware } from '@nestjs/common';
    import { NextFunction, Request, Response } from 'express';
    import { UsersService } from 'src/users/users.service';
    import { JwtService } from './jwt.service';
    
    @Injectable()
    export class JwtMiddleWare implements NestMiddleware {
      constructor(
        private readonly jwtService: JwtService,
        private readonly userService: UsersService,
      ) {}
      async use(req: Request, res: Response, next: NextFunction) {
        if ('x-jwt' in req.headers) {
          const token = req.headers['x-jwt'];
          const decodedToken = this.jwtService.verifyToken(token.toString());
          if (
            typeof decodedToken === 'object' &&
            decodedToken.hasOwnProperty('id')
          ) {
            try {
              const user = await this.userService.findById(decodedToken['id']);
              req['user'] = user;
            } catch (error) {}
          }
        }
        next();
      }
    }

    우리는 NestMiddleware interface를 참조한 JwtMiddleware를 만듭니다. 여기에서 필요한 것은 우리가 만든 JwtService에서 인증을 해야하기 때문에 verifyToken 함수가 필요할 것이고, UserService에서 id로 데이터베이스 내 유저를 찾는 findById라는 함수가 필요합니다.

     


    code flow

    express로 부터 받아온 req, res, next를 통해 위와 같은 로직을 통과시킨 뒤, next()를 호출합니다.

     

    우리는 http headers에 있는 x-jwt를 찾습니다. 만약 이 것이 존재한다면 우리는 jwtService의 verifyToken 함수를 이용해 토큰을 디코딩합니다.

     

    만약 이 디코딩된 토큰의 내용이 object라면 우리는 이 중 id값을 찾습니다.

     

    그리고 이 모든것이 true로 리턴되면, 우리는 이 아이디로 데이터베이스 내의 동일한 아이디를 가진 유저를 찾습니다.

     

    'user' 라는 이름의 http request를 찾아온 User 오브젝트에 담아 보냅니다.

     

     

    우리는 다음과 같은 기능을 가진 jwtMiddleware를 만들었습니다. 여기까지 했다면 우린 거의 모든 인증을 마쳤습니다. 이제 남은 것은 무엇일까요? http header에 포함된 user 오브젝트를 찾기만 하면 됩니다. 저는 이 프로젝트를 graphql로 진행했기 때문에 graphql 모듈에 context를 활용하여 header를 찾아 return하였습니다.

     

     

    graphql context

     GraphQLModule.forRoot({
          autoSchemaFile: true,
          context: ({req}) => ({user: req['user']}),
        }),

    이제 user에 대한 header를 받아온 후 리턴하게 되면 우리는 백엔드로부터 인증된 유저의 정보를 받아 올 수 있게 됩니다!

     

     

    이번 포스팅은 jwt work flow를 전체적으로 살펴보고, 공부해보는 포스팅이였습니다! 유저 인증의 기본인 jwt token을 활용한 로그인 인증을 해보았습니다. nest에서는 passport라는 인증 라이브러리가 있습니다. 나중에는 passport를 이용한 인증처리 포스팅을 해보도록 하겠습니다! 감사합니다 좋은 하루 되세요 ^^*

    댓글

sangjun's blog