[Node.js] JWT 구현하기 예제

charming__kyu·2021년 5월 3일
15

JWT

목록 보기
1/1
post-thumbnail

Intro

웹 / 앱 개발을 하면 로그인 과정에서 반드시 만나게 되는 개념이 쿠키-세션이다.
이미 많은 자료와 경험으로 인해 쿠키는 나쁜 놈 세션은 좋은 놈, 로그인은 일단 세션으로 해야지라는 개념이 개발자들의 머릿속에 자리 잡혀있다. 그러나, 최근 들어 IT 인프라 구성에 많은 변화가 생겼다. 웹 기반의 서비스들은 웹과 앱을 함께 서비스하는 것을 넘어 ‘Mobile First’ 앱이 먼저라는 인식까지 생겨났다. 또한, AWS, Azure 와 같은 클라우드 서비스가 대중화 되면서 고사양 단일 서버 아키텍쳐에서 중-저사양 다중 서버 아키텍쳐로 변화하고 있다. 이러한 상황에서 더 이상 쿠키-세션 기반 인증 아키텍쳐는 현재의 요구사항을 만족하지 못하고 있다.
현재의 요구 사항을 그나마 충족시키는 Web Token 기반 JWT에 대해서 알아보고 Node.js Express를 이용해 간단히 구현 해보고자 한다.

JWT의 기본 개념

JSON Web Token

JSON Web Token (JWT) 은 웹표준 (RFC 7519) 으로서 두 개체에서 JSON 객체를 사용하여 가볍고 자가수용적인 (self-contained) 방식으로 정보를 안전성 있게 전달해줍니다.

수많은 프로그래밍 언어에서 지원됩니다

JWT 는 C, Java, Python, C++, R, C#, PHP, JavaScript, Ruby, Go, Swift 등 대부분의 주류 프로그래밍 언어에서 지원됩니다.

자가 수용적 (self-contained) 입니다

JWT 는 필요한 모든 정보를 자체적으로 지니고 있습니다. JWT 시스템에서 발급된 토큰은, 토큰에 대한 기본정보, 전달 할 정보 (로그인시스템에서는 유저 정보를 나타내겠죠?) 그리고 토큰이 검증됐다는것을 증명해주는 signature 를 포함하고있습니다.

쉽게 전달 될 수 있습니다

JWT 는 자가수용적이므로, 두 개체 사이에서 손쉽게 전달 될 수 있습니다. 웹서버의 경우 HTTP의 헤더에 넣어서 전달 할 수도 있고, URL 의 파라미터로 전달 할 수도 있습니다.

어떤 상황에서 사용될까?

회원 인증

JWT 를 사용하는 가장 흔한 시나리오 입니다. 유저가 로그인을 하면, 서버는 유저의 정보에 기반한 토큰을 발급하여 유저에게 전달해줍니다. 그 후, 유저가 서버에 요청을 할 때 마다 JWT를 포함하여 전달합니다. 서버가 클라이언트에게서 요청을 받을때 마다, 해당 토큰이 유효하고 인증됐는지 검증을 하고, 유저가 요청한 작업에 권한이 있는지 확인하여 작업을 처리합니다. 즉, 서버측에서는 유저의 세션을 유지 할 필요가 없습니다. 유저가 로그인되어있는지 안되어있는지 신경 쓸 필요가 없고, 유저가 요청을 했을때 토큰만 확인하면 되니, 세션 관리가 필요 없어서 서버 자원을 많이 아낄 수 있습니다.

정보 교류

JWT는 두 개체 사이에서 안정성있게 정보를 교환하기에 좋은 방법입니다. 그 이유는, 정보가 sign 이 되어있기 때문에 정보를 보낸이가 바뀌진 않았는지, 또 정보가 도중에 조작되지는 않았는지 검증할 수 있습니다.

JWT의 내용

JWT의 생성

JWT 토큰을 만들때는 JWT 를 담당하는 라이브러리가 자동으로 인코딩 및 해싱 작업을 해줍니다. 이 포스트에서는 NPM 라이브러리인 jsonwebtoken 을 사용하여 HMAC SHA256 인코딩 및 해싱하는 과정을 구현해보고자 한다.


JWT는 헤더(header) , 정보(payload) , 서명(signature) 구조로 이루어져 있다.

구조

Header : 타입(JWT)과 알고리즘(BASE64 같은)을 담는다.
Payload : 보통 유저정보(id같은)와 만료기간이 객체형으로 담긴다.
Signature : header, payload를 인코딩 한 값을 합친뒤 SECRET_KEY로 해쉬한다.

등록된 클레임

iss : 토큰 발급자 (issuer)
sub : 토큰 제목 (subject)
aud : 토큰 대상자 (audience)
exp : 토큰의 만료시간 (expiraton), 시간은 NumericDate 형식으로 되어있어야 하며 (예: 1480849147370) 언제나 현재 시간보다 이후로 설정되어있어야합니다.
nbf : Not Before 를 의미하며, 토큰의 활성 날짜와 비슷한 개념입니다. 여기에도 NumericDate 형식으로 날짜를 지정하며, 이 날짜가 지나기 전까지는 토큰이 처리되지 않습니다.
iat : 토큰이 발급된 시간 (issued at), 이 값을 사용하여 토큰의 age 가 얼마나 되었는지 판단 할 수 있습니다.
jti : JWT의 고유 식별자로서, 주로 중복적인 처리를 방지하기 위하여 사용됩니다. 일회용 토큰에 사용하면 유용합니다.

추가로 더 필요한 개념은 [JWT] JSON Web Token 소개 및 구조을 참고바랍니다.

Node Express로 JWT 구현

가정

웹페이지는 CSR(Client Side Rendering) 이라는 가정하에 Node서버는 오로지 REST API 기능만 제공한다고 했을 때 ExpressPostman 으로 간단하게 구현을 해볼 예정이다. JTW의 인증 순서는 아래와 같이 정한다.

예제

Create Token with secret key

const jwt = require('jsonwebtoken');
const SECRET_KEY = 'MY-SECRET-KEY';

// POST /login 요청 body에 id와 password를 함께 실어서 요청으로 가정 (사실 id와 password는 암호화 되어있음)
router.post('/login', (req, res, next) => {

  //받은 요청의 id와 password로 DB에서 프로필사진, 닉네임 등 로그인 정보를 가져온다.
  const nickname = "CharmingKyu";
  const profile = 'imageURL';

  //jwt.sign(payload, secretOrPrivateKey, [options, callback])
  token = jwt.sign({
    type: 'JWT',
    nickname: nickname,
    profile: profile
  }, SECRET_KEY, {
    expiresIn: '15m', // 만료시간 15분
    issuer: '토큰발급자',
  });

  //response
  return res.status(200).json({
    code: 200,
    message: '토큰이 발급되었습니다.',
    token: token
  });
});

POST /login 요청이 유저의 ID와 PASSWORD와 함께 들어오면 DB에서 사용자의 간단한 로그인 정보는 Payload부분에 적재를 하지만 사용자의 비밀번호나 개인정보는 적재하지 않는게 좋다. (마음만 먹으면 쉽게 풀 수 있다.) 그리고 너무 길이가 긴 데이터는 적재하면 토큰이 너무 무거워지기 때문에 JWT의 장점을 살릴 수가 없게 된다. jsonwebtoken 에 정의되어 있는 sign함수로 token을 생성한다. 예제에서 사용한 SECRET_KEYenv 로 설정을 해두면 편할 것 같다.

authMiddleware.js

const jwt = require('jsonwebtoken');
const SECRET_KEY = 'MY-SECRET-KEY';
exports.auth = (req, res, next) => {
    // 인증 완료
    try {
        // 요청 헤더에 저장된 토큰(req.headers.authorization)과 비밀키를 사용하여 토큰을 req.decoded에 반환
        req.decoded = jwt.verify(req.headers.authorization, SECRET_KEY);
        return next();
    }
    // 인증 실패
    catch (error) {
        // 유효시간이 초과된 경우
        if (error.name === 'TokenExpiredError') {
            return res.status(419).json({
                code: 419,
                message: '토큰이 만료되었습니다.'
            });
        }
        // 토큰의 비밀키가 일치하지 않는 경우
        if (error.name === 'JsonWebTokenError') {
            return res.status(401).json({
                code: 401,
                message: '유효하지 않은 토큰입니다.'
            });
        }
    }
}

토큰 생성까지 되었다면 요청이 들어왔을 때 토큰이 유효한지 체크하는 미들웨어를 추가한다.
그리고 아래 코드는 위에 작성한 미들웨어로 실제 요청이 들어왔을 때 어떻게 처리하는지 보여주는 예제이다.

Check Token Signature

const { auth } = require('./authMiddleware');
const SECRET_KEY = 'MY-SECRET-KEY';
router.get('/payload', auth, (req, res) => {
  const nickname = req.decoded.nickname;
  const profile = req.decoded.profile;
  return res.status(200).json({
    code: 200,
    message: '토큰은 정상입니다.',
    data: {
      nickname: nickname,
      profile: profile
    }
  });
});

router.get() 두번째 인자에 auth 를 삽입하게 되면 해당 API에 요청이 들어왔을 때 아래 코드가 실행되기 전에 분기를 auth 부분으로 먼저 보내게 된다. 만약 토큰에 문제가 있다면 미들웨어 부분에서 Error reponse를 반환하기 때문에 분기가 끝나버리게 된다. 만약 토큰에 문제가 없다면 next() 함수로 인해 분기가 GET /payload 다시 복귀를 하게 되어서 토큰은 정상이라는 메세지와 payload 값을 반환하게 된다.


꼭 http요청 header 부분에 authorization 값에 POST /login 에서 받은 JWT를 넣고 요청을 보낸다.

정상적으로 토큰 검증을 끝내고 payload 값을 반환한다.

만약 토큰 시간이 만료되었다면 정상적으로 토큰 만료를 알리게 된다.

profile
러닝커브를 낮추는데 노력하는 개발자입니다.

2개의 댓글

comment-user-thumbnail
2021년 5월 4일

좋은 글 감사합니다..

1개의 답글