(Node.js) JWT 인증을 활용한 Express 서버 만들기

호두파파·2021년 8월 2일
5

Node.js

목록 보기
14/25

JWT(JSON WEB Token)

JWT는 JSON 형식의 데이터를 저장하는 토큰이다. 헤더,페이로드, 시그니처 세 부분으로 구성되어 있다.

  • 헤더 : 토큰 종류, 해시 알고리즘 정보
  • 페이로드 : 토큰 내용물이 인코딩된 부분, 내용을 볼 수 있으므로 민감한 정보는 넣지 않아야 한다.
  • 시그니처 : 토큰 변조 유무 확인을 위한 일련의 문자열, 시그니처는 숨기지 않아도 되나 JWT 비밀키로 생성되므로 비밀키는 반드시 숨겨야 한다.

장점

  • 서버는 JWT 토큰 검증만 하면 되기 때문에 서버에 별도의 저장소를 마련할 필요가 없다.
    -JWT 비밀키를 알지 않는 이상 위변조가 불가능하므로 내용물이 바뀌지 않았는지 걱정할 필요가 없다. 다만 외부에 노출되어도 좋은 정보에 한해서 사용하는 것을 권장한다.

단점

  • 한 번 발급된 JWT 토큰은 유효기간이 만료될 때까지 사용이 가능하다. 유효기간이 지나기 전에 탈취된 토큰이 악용될 수 있다.
  • 내용(페이로드)이 암호화되지 ㅇ낳으므로 누구나 내용을 볼 수 있다.
  • 세션/쿠키 방식에 비해 JWT 토큰의 길이가 길기 때문에 토큰 발급 및 검증 요청이 많아질수록 서버의 자원 낭비가 발생할 수 있다.

준비

npm 패키지 설치

필요한 npm 패키지는 express-generator, dotenv, jsontokenweb 이다.

  • express-generator는 Express 서버 프로젝트를 자동으로 생성해준다.
  • dotenv는 환경변수를 쉽게 사용할 수 있도록 해준다.
  • jsontokenweb은 JWT 인증을 사용하기 위해 필요하다.

npm i express-generator dotenv jsontokenweb명령어로 실행해 필요한 npm 패키지를 설치한다.

Postman 설치

JWT 토큰 발급 시 POST 요청을, 발급된 토큰을 테스트할때 GET 요청을 할것이다. 이런 요청을 보내고 테스트 하기 위해 필요한 프로그램이 POSTMAN이다.


JWT 토큰 발급

환경변수

JWT 인증에 사용할 비밀키를 환경변수에 등록해준다. 비밀키는 소스코드에 입력해도 되나, 외부에 쉽게 노출되지 않도록 환경변수에 등록해 관리하는 것이 좋다.

Express 서버 프로젝트의 루트 디렉토리에 .env파일을 생성한 후 JWT 인증에 사용할 비밀키를 입력해준다.

JWT_SECRET=JwTsEcReTkEyOrHaShInG

토큰 유효성 검증

JWT 토큰이 유효한지 검사하는 메서드를 만든다.

const jwt = require('jsonwebtoken');

exports.verifyToken = (req, res, next) => {
  // 인증 완료
  try {
    req.decoded = jwt.verify(req.headers.authorization, process.env.JWT_SECRET)
    return next();
  }
  
  // 인증 실패 
  catch(error) {
    if (error.name === 'TokenExpireError') {
      return res.status(419).json({
        code: 419,
        message: '토큰이 만료되었습니다.'
      });
    }
   return res.status(401).json({
     code: 401,
     message: '유효하지 않은 토큰입니다.'
   });
  }
}

토큰 발급

jsonwebtoken의 sign()메서드로 JWT 토큰을 발급한다. 이때 토큰에 들어갈 내용(페이로드)과 비밀키 그리고 옵션을 넣어준다.

  • localhost:3000/token주소로 POST 요청 시 토큰을 발급하는 라우터를 만들었다.
  • localhost:3000/token/test주소로 GET요청시 발급된 토큰을 테스트하는 라우터를 만들었다.
/*** routes/token.js ***/
const express = require('express');
const jwt = require('jsonwebtoken');
require('dotenv').config();

const { verifyToken } = require('./middlewares');

const router = express.Router();

router.post('/', async (req, res) => {
  try {
    const id = 'vappet'
    const nick = 'hodoopapa'
    // jwt.sign() 메소드: 토큰 발급 
    const token = jwt.sign({
      id, 
      nick, 
    }, process.env.JWT_SECRET, {
      expireIn: '1m' //1분
      issuer: '토큰 발급자'
    });
    
    return res.json({
      code: 200,
      message: '토큰이 발급되었습니다.',
      token,
    });
    catch (error) {
      console.error(error);
      return res.status(500).json({
        code: 500,
        message: '서버 에러',
      });
    }
  });
  
  router.get('/test', verifyToken, (req, res) => {
    res.json(req.decoded);
  });
  
  module.exports = router;

이렇게 만든 라우터를 app.js에 연결한다.

/*** app.js ***/
const tokenRouter = require('./routes/token');
//...
app.use('/token', tokenRouter);
//...

토큰 발급 테스트

Postman에서 다음과 같이 설정한 후 Send 버튼을 클릭해 JWT 토큰을 발급 받는다.

  • request 종류 : POST
  • 접속주소 : localhost:3000/token

다음과 같은 응답이 돌아온다. 여기서 token 값이 JWT 토큰이다.

{ 
  "code": 200,
  "message": "토큰이 발급되었습니다."
  "token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
}

토큰 유효성 테스트

Postman에서 다음과 같이 설정한 후 Send 버튼을 클릭해 발급된 JWT 토큰을 테스트한다.

  • request 종류 : GET
  • 접속주소 : localhost:3000/token
  • Headers 탭에서 KEY에 authorization을, value에 방금 전 발급받은 JWT 토큰 값을 입력한다.

토큰이 유효한 경우 내용(페이로드)이 포함된 응답이 돌아옵니다.

{
    "id": "vappet",
    "nick": "hodoopapa",
    "iat": 1581831357,
    "exp": 1581831477,
    "iss": "토큰발급자"
}

토큰이 유효하지 않은 경우, 즉 발급한지 1분이 지난 토큰을 보낸 경우 토큰이 만료되었다는 응답이 돌아온다.

{
  "code": 419,
  "message": "토큰이 만료되었습니다."
}

JWT 토큰 인증을 사용한 API 서버 만들기

API 라우터 생성

/*** routes/api.js ***/
const express = require('express');
const router = express.Router();

const { verifyToken } = require('./middlewares');

router.get('/',  verifyToken, (req, res) => {
  const users = [
    { id: 1, name: 'Node.js' },
    { id: 2, name: 'npm' },
    { id: 3, name: 'Pengsu' },
  ]
  // 모든 정보 제공 
  res.json(users);
});

router.get('/:id', verifyToken, async(req, res) => {
  const users = [
    { id: 1, name: 'Node.js' },
    { id: 2, name: 'npm' },
    { id: 3, name: 'Pengsu' },
  ]
  // 특정 정보를 찾아 제공 
  user = users.find(u => u.id === parseInt(req.params.id));
  res.send(user);
});

module.exports = router;

이렇게 만든 라우터를 app.js에 연결한다.

/*** app.js ***/
//...
const tokenRouter = require('./routes/token');
const apiRouter = require('./routers/api);
//...
app.use('/token', tokenRouter);
app.use('/api', apiRouter);
//...

토큰 및 API 사용량 제한하지

express-rate-limit 패키지

express-rate-limit 패키지를 사용하면 Express 서버에 접속하는 클라이언트의 사용량을 제한할 수 있다.
npm i express-rate-limit명령어를 사용해 해당 패키지를 설치한다.

리미터 생성

토큰 및 API 사용을 제한하는 리미터 메서드를 생성한다. 여기서는 토큰을 1분당 한 번, API를 1분에 최대 5번 호출하도록 제한하는 리미터를 만들어보자.

const RateLimit = require('express-rate-limit');

exports.tokenLimiter = new RateLimit({
  windowMs: 1000 * 60, // 기준 시간 (1000ms * 60 = 1분)
  max: 1, // 허용 횟수
  delayMs: 0, // 호출 간격
  handler(req, res) { // 제한 초과 시 콜백함수
    res.status(this.statusCode).json({
      code: this.statusCode, // 기본값: 429
      message: '1분에 한 번만 요청할 수 있습니다.',
    });
  },
});

exports.apiLimiter = new RateLimit({
  windowMs: 1000 * 60, // 기준 시간 (1000ms * 60 = 1분)
  max: 5, // 허용 횟수
  delayMs: 0, // 호출 간격
  handler(req, res) { // 제한 초과 시 콜백함수
    res.status(this.statusCode).json({
      code: this.statusCode, // 기본값: 429
      message: '1분에 최대 다섯 번 요청할 수 있습니다.',
    });
  },
});

리미터 연결

라우터에 리미터 메서드를 콜백함수로 연결한다.

const express = require('express');
const jwt = require('jsonwebtoken');
require('dotenv').config();

const { verifyToken, tokenLimiter } = require('./middlewares');

const router = express.Router();

// 토큰 발급 라우터에 tokenLimiter 연결
router.post('/', tokenLimiter, async(req, res) => {
  // 생략..
});

module.exports = router;
const express = require('express');
const router = express.Router();

const { verifyToken, apiLimiter } = require('./middlewares');

// API 제공 라우터에 apiLimiter 연결 
router.get('/', apiLimiter, verifyToken, (req, res) => {
  // 생략..
});

router.get('/:id', apiLimiter, verifyToken, async(req, res) => {
  // 생략 ...
});

module.exports = router;

출처

Node.js 교과서(조현영)

profile
안녕하세요 주니어 프론트엔드 개발자 양윤성입니다.

1개의 댓글

comment-user-thumbnail
2023년 6월 12일

ㅇ낳으므로 -> 않으므로 오타 있어요

답글 달기