5 minute read

[OAuth] 같이 참고하시면 좋을 OAuth에 관한 포스트입니다.

[JWT]

1. JWT(JSON Web Token) 생성과 검증

앞선 포스트에서 JWT의 개념과 구조, 주요 용어에 대해 설명드렸습니다. 이번 포스트에서는, 실제로 코드를 작성해서 어떻게 검증이 이루어지는 지 살펴보겠습니다. 아주 재밌습니다.

1-1. nest project 생성

저는 실제로 API를 호출하는 실습까지 해보고자 node의 nest.js 환경에서 코드를 작성해보았습니다.

$npm i -g @nestjs/cli // nest설치

$nest new jwt-practice // jwt-practice라는 이름으로 프로젝트 생성

1-2. jsonwebtoken 라이브러리 설치

저는 jsonwebtoken 라이브러리를 사용할 건데요, JWT를 지원하는 다양한 라이브러리가 있지만, 저는 이 라이브러리가 가장 쉬웠습니다. 여기(jwt.io)에서 언어별로 지원하는 라이브러리와 스택이 상세하게 안내되어 있습니다.
여기(jsonwebtoken)에서는 이 jsonwebtoken 라이브러리에 대한 안내가 있습니다.

$ npm install jsonwebtoken

명령어로 jsonwebtoken을 설치해 줍니다.

설치를 마쳤으면, 이제 이 라이브러리를 sign(jwt 생성), verify(jwt 검증)를 실습해 보겠습니다.

2. sign(jwt 생성)

jwt를 생성하기 전에 결정해야하는 것이 몇가지 있습니다. 지난번 포스트에서 header와 payload가 어떻게 생겼었는지 살펴봤었는데, 혹시 기억 나시나요? header는 다음과 같이 생겼었고,

{
  "alg": "HS256",
  "typ": "JWT"
}

payload는 다음과 같이 생겼었죠.

{
  "firstName": "hannah",
  "iat": 1657989649,
  "exp": 1657989652
}

header엔 보시다시피 토큰의 타입과, 서명에 쓰인 알고리즘이 포함되어 있습니다. 그리고 signature는 header와 payload 두 부분을 합쳐 secretKey로 해싱한 값이었습니다. 그럼 여기서 우리가 필요한 건? 서명에 사용할 secretKey와, 알고리즘을 결정해야 겠지요! 이 두가지는 필수입니다. (라이브러리에서는 서명 알고리즘으로 HS256을 default값으로 지정하고 있기는 합니다.)

두번째로, 서명의 내용물에 해당하는 payload에 들어갈 claim들을 결정해야 합니다. 또한 jwt에 내장된 만료기능을 사용하기 위해서, 만료시간을 지정하는 게 좋을 것 같습니다.

정리해보자면,

  1. SecretKey (보안상 절대 유출 되선 안됩니다)
  2. 서명 알고리즘 (HS256, RS256, 등등)
  3. payload안에 들어갈 claim (ex. “firstName”: “hannah”)
  4. 역시 payload안에 들어갈 claim (“exp”: 1657989652)

3번과 4번을 따로 분류한 이유는, 3번은 서명을 협의하는 과정에서 만들어지는 비공개(private) 클레임이고, 4번은 토큰의 정보를 담기 위해 이미 정해진(registered) 클레임이기 때문입니다. 4번에 해당하는 표준스펙 클레임들은 앞선 포스트에서 정리한 바 있습니다. iss, sub, exp, nbf.. 등등입니다.

3번에 저는 firstName이라는 claim을 임의로 지정하였는데요, 서명 검증을 마친 후 firstName에 해당하는 정보만 돌려주는 간단한 기능을 만들 것이기 때문입니다. hannah에겐 hannah의 정보만, mina에겐 mina의 정보만 주는 방식입니다.

//appController
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
import { sign, verify } from 'jsonwebtoken'; //jsonwebtoken 라이브러리 import

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

  @Get('/issueJwt')
  issue() {
    const payload = {           // payload 생성
      firstName: 'hannah',
    };

    const jwtToken = sign(payload, 'thisIsSecretKey', {   // jwt 토큰생성
      algorithm: 'HS256',   // 토큰 생성에 사용할 알고리즘
      expiresIn: '3s',      // 토큰의 유효기간, '2 days', '10h', '7d' 이런 식으로도 가능합니다.
    });
    return jwtToken;
  }
}

저는 payload로 {firstName: ‘hannah’}를 작성하였고, secretKey로는 ‘thisIsSecretKey’를 설정하였으며, 알고리즘은 HS256을 사용하였고, 만료시간은 3초로 설정하였습니다.

이렇게 작성한 후 다음의 명령어로 실행해줍니다.

$npm run start:dev

그리고 브라우저를 켜서 localhost:3000/issueJwt를 접속하면!

Alt text 토큰 발급이 완료되었습니다. 이 토큰을 복사해서 jwt.io여기에서 확인해봅시다.

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmaXJzdE5hbWUiOiJoYW5uYWgiLCJpYXQiOjE2NTgwMjQyODYsImV4cCI6MTY1ODAyNDI4OX0.SwxbhIYHJmz5LBJcMOLFbwKWoPv9fop4pD6f7hCTpCM


Alt text

잘 생성된 걸 알 수 있습니다. expiresIn을 지정해 주었더니 payload안에 iat(발급시간), exp(만료시간)이 담겼습니다. 이제 유효성 검증만이 남았습니다.

3. verify(jwt 검증)

  @Get('/verify')
  async verify() {
    const jwt =
      'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmaXJzdE5hbWUiOiJoYW5uYWgiLCJpYXQiOjE2NTgwMjY0MTUsImV4cCI6MTY1ODAzMDAxNX0.9F_a6GE4FVi1J0EPzTQ0UPx4PcCI64HsjhtbddDlWiI';
    try {
      const payload = verify(jwt, 'thisIsSecretKey', { algorithm: 'HS256' }); //검증되는 부분. 유효성이 입증되면 payload를 반환
      return payload.firstName; // payload 안의 firstName을 리턴
    } catch (e) {
      return e.message; //오류가 발견되면 오류 메시지를 리턴
    }
  }

검증 코드입니다. sign에서 만든 jwt를 소스 안에 넣었습니다. 여기서 주목할 함수는 verify이고, 여기에서 유효성 검증이 이뤄집니다. 모든 에러를 다 뚫고 유효성이 검증되면 payload를 반환하고, 우리는 payload.firstName으로 ‘hannah’를 꺼낼 수 있게 되는 것입니다. 다시 한번 브라우저를 켜봅시다.
Alt text

검증 결과 유효성이 입증되었고 payload 안에있던 firstName을 리턴받았습니다.

3-1 유효성 테스트(1) JWT형태가 아니면?

jwt가 (.)으로 세 부분으로 나뉘어져 있는 거 기억나시죠? 그럼 여기서 (.)을 하나 빼본다면 어떻게 될까요?

  @Get('/verify')
  async verify() {
    const jwt =                           
      'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9eyJmaXJzdE5hbWUiOiJoYW5uYWgiLCJpYXQiOjE2NTgwMjY0MTUsImV4cCI6MTY1ODAzMDAxNX0.9F_a6GE4FVi1J0EPzTQ0UPx4PcCI64HsjhtbddDlWiI';
    try {
      const payload = verify(jwt, 'thisIsSecretKey', { algorithm: 'HS256' }); //검증되는 부분. 유효성이 입증되면 payload를 반환
      return payload.firstName; // payload 안의 firstName을 리턴
    } catch (e) {
      return e.message; //오류가 발견되면 오류 메시지를 리턴
    }
  }

같은 코드에서, 첫번째 (.) 하나를 빼보겠습니다. 그리고 실행하면,
Alt text
jwt malformed라는 에러메시지를 반환합니다. jwt처럼 안생겼다는 의미지요. jsonwebtoken 라이브러리를 한번 보시면,

  var parts = jwtString.split('.');

  if (parts.length !== 3){
    return done(new JsonWebTokenError('jwt malformed'));
  }

이런 코드가 작성되어 있습니다. (.)으로 토큰을 나눠서, 총 파트가 3개가 아니면 오류를 리턴하는 코드입니다. 이렇게 jsonwebtoken라이브러리는 다양한 유효성 체크 기능을 제공하고 있어서, 우리는 감사하게도 라이브러리만 사용하면 됩니다.

3-2 유효성 테스트(2) 시간이 만료되면?

 @Get('/giveInfo')
  async giveInfo() {
    /////////////////////////// 여기부터 ////////////
    const makeJWT = () => {
      const payload = {
        firstName: 'hannah',
      };

      const jwtToken = sign(payload, 'thisIsSecretKey', {
        algorithm: 'HS256',
        expiresIn: '3s',
      });
      return jwtToken;
    };
    const jwt = makeJWT();
    //////여기까지는 앞에서 봤던 JWT생성 코드입니다. API를 호출하면, 유효시간이 3초인 토큰을 하나 만들겁니다.

  
    ////////////////다시 여기서부터 ///////////////////
    const sleep = (delay) =>
      new Promise((resolve) => setTimeout(resolve, delay));
    await sleep(4000);
    ///////////여기까지는 JWT생성 후 잠시 프로세스를 멈추기 위해 만든 코드입니다. 
    /////////sleep(2000)을 입력하면 2초를 쉬게되고, sleep(4000)을 입력하면 4초를 쉴 것입니다. 
    ///////// 4초가 지나면... 토큰의 유효시간은 만료가 되겠죠?

    try {
      const payload = verify(jwt, 'thisIsSecretKey', { algorithm: 'HS256' });
      if (payload.firstName === 'hannah') {
       return payload.firstName;
      }
    } catch (e) {
      return e.message;
    }
  }

await sleep(4000); 으로 작성해서, 토큰이 만들어지고 4초 후에 검증을 시작하는 코드를 실행해보겠습니다. api를 호출하면, 유효기간 3초짜리 토큰 생성 -> 4초 휴식 -> 검증 시작의 flow가 만들어지게 됩니다.


Alt text

jwt가 만료되었다고 나옵니다.

3-3 활용 케이스

마지막으로 어떻게 활용할 수 있는지 실습해보고 마치겠습니다. 방금 3-2에서 본 코드에서, 검증 후 리턴부분만 살짝 변경해보겠습니다. 주석을 잘 읽어주시기 바랍니다.

 @Get('/giveInfo')
  async giveInfo() {
 /////////////////////////// 여기부터 ////////////
    const makeJWT = () => {
      const payload = {
        firstName: 'hannah',  ////////////////////////////////이름을 hannah로 설정
      };

      const jwtToken = sign(payload, 'thisIsSecretKey', {
        algorithm: 'HS256',
        expiresIn: '3s',
      });
      return jwtToken;
    };
    const jwt = makeJWT();
    //////여기까지는 앞에서 봤던 JWT생성 코드입니다. API를 호출하면, 유효시간이 3초인 토큰을 하나 만들겁니다.

  
    ////////////////다시 여기서부터 ///////////////////
    const sleep = (delay) =>
      new Promise((resolve) => setTimeout(resolve, delay));
    await sleep(2000);
    ///////////여기까지는 JWT생성 후 잠시 프로세스를 멈추기 위해 만든 코드입니다. 
    /////////유효시간을 테스트하는 것이 아니므로, 유효성 통과를 위해 2초로 설정해두겠습니다.

    try {
      const payload = verify(jwt, 'thisIsSecretKey', { algorithm: 'HS256' });
      if (payload.firstName === 'hannah') {           // 이름이 hannah이면
        return 'very important info about hannah';    // hannah의 매우 중요한 정보를 return
      } else {                                        // 그렇지 않으면
        return 'you are not hannah';                  // 당신은 hannah가 아닙니다, 를 return
      }
    } catch (e) {
      return e.message;
    }
  }

API호출 시 flow는 다음과 같습니다.

  1. firstName을 hannah로 설정한 유효시간 3초 jwt토큰 생성
  2. 2초 휴식
  3. 휴식 후 검증
  4. 검증 후 verify함수는 payload를 반환함
  5. payload에서 이름을 꺼내서, 이름이 hannah이면 정보를 return, hannah가 아니면 당신은 hannah가 아닙니다를 return.

실행해보면, payload의 firstName을 hannah로 설정하는 경우 아래의 값을 리턴하고,
Alt text

payload의 firstName을 mina로 설정하는 경우 아래의 값을 리턴합니다.
Alt text

고생하셨습니다.

참고

JWT의 개념과 구조, 주요 용어

jwt.io

jsonwebtoken


광고


Comments