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

 

 

node express로 JWT 구현

가정

웹페이지는 CSR(Client Side Rendering)이라는 가정하에 Node서버는 오로지 REST API 기능만 제공한다고 했을 때 Express와 Postman으로 간단하게 구현을 해볼 예정이다. 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_KEY는 env로 설정을 해두면 편할 것 같다.

Postman Request 결과

 

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 값을 반환한다.

 

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