Java / / 2022. 8. 3. 10:46

[node.js] JWT 토큰 발급 & JWT 미들웨어 설정

반응형

JWT란?

JSON Web Token
웹 표준으로써 두 개체에서 JSON객체를 사용하여 정보를 안전성 있게 전달한다.

 

JWT가 사용되는곳

회원인증 (Authenticated)

  • 유저가 로그인을 하면 서버는 유저의 정보에 기반한 Token을 발급해서 유저에게 전달하고, 유저는 서버에 요청을 할 때마다  Token을 전달하는 식으로 검증한다.
  • 즉, 유저가 요청을 했을 때만 토큰을 확인하면 되니 세션 관리가 필요 없어서 서버의 자원을 아낄 수 있다.

 

JWT 인증 절차

 


 

 

로그인 요청 시 JWT 발급

1. route.js - (id/pw로 client가 로그인 요청)

router.post('/admin/login', LoginController.login);

 

2.loginController.js 

const express = require("express");
const logger = require('../../config/logger');
const loginModel = require('../../models/admin/login');

module.exports = {
    /**
     * 관리자페이지 로그인
     * @param {*} req
     * @param {*} res
     **/
    login: async (req,res) => {
        const result = await loginModel.login(req,res);
        res.json(result);
    }
}

 

3. login.js

const express = require('express');
const pool = require('../db');
const logger = require('../../config/logger');
const jwt = require('../../config/jwt');

module.exports = {

    login: async (req,res) => {

        const connection = await pool.getConnectionPool(async conn => conn);
        
        try {
            const id = req.body.id;
            const pw = req.body.password;
            let resultData = "";

            const selectIdSql = //admin id로 조회하여 count값이 0이상이 나오는지 확인하는 쿼리;
            const [data] = await connection.query(selectIdSql, id);
                if (data[0].result < 1) {
                    connection.release();
                    resultData = ([{status : 401, msg: '입력하신 아이디가 없습니다.'}]);
                    res.json(resultData);
                } else {
                    const selectIdSql2 = //admin id로 값이 있다는 것을 확인 했다면, pw가 일치하는지 확인하는 쿼리;
                    const param = [id,pw];
                    const [data] = await connection.query(selectIdSql2, param);
                    if (data[0].result < 1) {
                        connection.release();
                        resultData = ([{status : 402, msg: '비밀번호가 맞지 않습니다.'}]);
                        res.json(resultData);
                    }else{
                        connection.release();

			//로그인 성공 시, 토큰을 발급하여 response로 넘겨준다
                        const acc_payload = {
                            id: req.body.id,
                            pw: req.body.password
                        }
                        const ref_payload = {
                            id: req.body.id,
                            pw: req.body.password
                        }

                        const access_token = jwt.token().access(acc_payload);//토큰발급
                        const refresh_token = jwt.token().refresh(ref_payload);

                        resultData = ([{status : 200, msg: '로그인 성공', ref_tkn: "Bearer " + refresh_token, acc_tkn: "Bearer " + access_token}]);
                        res.json(resultData);
                    }
                }

        } catch (e) {
            connection.release();
            res.json([{status : 500, msg : '시스템 오류가 생겼습니다'}]);
        }
    }
}
  • 로그인을 성공하면 payload에 id와 pw정보를 담아서 jwt를 발급한다.

 

4. jwt.js

require("dotenv").config();
const jwt = require('jsonwebtoken');
const logger = require("./logger");

exports.token = () => {
    return {
        access(payload) {
            return jwt.sign({ payload }, process.env.ACCESS_TOKEN_SECRET, {
                expiresIn: "30m",
            });
        },
        refresh(payload) {
            return jwt.sign({ payload }, process.env.REFRESH_TOKEN_SECRET, {
                expiresIn: "360 days",
            });
        },
        getParsing(token) {
            const base64_payload = token.split('.')[1];
            const payload = Buffer.from(base64_payload, 'base64');
            const result = JSON.parse(payload.toString());
            return result;
        },
        async getTokenChk(token, value) {
            try {
                let secret;
                if (value === 'access') {
                    secret = process.env.ACCESS_TOKEN_SECRET
                } else {
                    secret = process.env.REFRESH_TOKEN_SECRET
                }
                const tokenVal = await jwt.verify(token, secret);


                return true;
            } catch (err) {
                logger.error(err);
                logger.error("Token Verify Error");
                return false;
            }

        }
    }
}

 

◎ jwt.sign(payload, secret, options, [callback])

  • payload - JWT에 저장되는 정보로 k -v로 구성된다.
  • secret - JWT에 사용되는 암호화 문자열이다. (따로 env파일에 설정해 두며 숨겨둔다.)
  • options - JWT에 사용되는 옵션이다. (알고리즘 default : HS256 / HS384, HS512, RS256, RS384, RS512, ES256, ES384, ES512, PS256, PS384 등 사용가능 )
    • expiresIn(exp) - JWT 만료시간으로 초 단위 또는 시간 범위 문자열로 설정한다.
      • 년 단위 : "1y"
      • 일 단위 : "1 days", "1d"
      • 시간 단위 : "2.5 hrs", "2h"
      • 분 단위 : "1m"
      • 초 단위 : "5s"
      • 단위 문자가 포함되지 않으면 초 단위로 설정한다.
    • notbefore(nbf) - JWT를 처리하지 않는 시간으로 초단위 또는 시간 범위 문자열로 설정한다.(expiresIn과 동일)
    • audience - JWT 사용할 수신자이다.
    • issuer - JWT 발급자이다. (도메인을 많이 사용함)
    • subject - claim의 주체이다.(JWT사용 목적이나 JWT 정보 객체단위를 사용)
    • jwtid - JWT 식별자이다.
  • callback - JWT가 생성된 후 호출되는 funcation이다. (생성된 토큰(token)이나 에러(err)를 인자로 사용)

 


 

중간에 React로 프론트 단에서 설명은 다른 포스팅으로 대체하겠습니다.

 

2022.08.08 - [Java Script] - 리액트 JWT 토큰과 localStorage를 이용한 로그인

 

리액트 JWT 토큰과 localStorage를 이용한 로그인

앞에서 JWT 토큰을 발급하는 방법을 살펴 보았다. 여기서는 발급 받은 JWT 토큰을 리액트에서 활용하는 방법을 작성해 보려고한다. 사실 쿠키를 사용하여 구현하려고 했지만, 구현이 생각처럼 잘

jjuun93.tistory.com

 

토큰 재발급 API

router.post("/admin/token/refresh", jwtController.tokenRefresh_admin);

 

jwtController.js

const logger = require('../../config/logger');
const express = require("express");
const jwt = require('../../config/jwt');
const jwtModel = require('../../models/api/jwtModel');

module.exports = {
    /**
     * jwt access admin 토큰 재발급 함수
     * @param {*} req(refresh 토큰)
     * @param {*} res(access 토큰)
     */
    tokenRefresh_admin: async (req, res) => {

        try {
            const req_token = req.get('ref_authorization');
            const req_refresh_token = req_token.substring(7);

            if (req_refresh_token === "null" || req_refresh_token === "") {
                //리프레시 토큰이 없을경우 로그인 페이지로 리다이렉트
                res.json(false);
            } else {
                //리프레시 토큰이 유효하면
                const auth_jwt = await jwt.token().getTokenChk(req_refresh_token, 'refresh');
                if (auth_jwt) {
                    const payload = jwt.token().getParsing(req_refresh_token)
                    const acc_payload = {
                        id: payload.payload.id,
                        pw: payload.payload.pw,
                    }
                    const access_token = jwt.token().access(acc_payload)

                    res.json({acc_token: "Bearer " + access_token});
                } else {
                    //리프레시 토큰은 있지만 유효하지 않을경우 로그인 페이지로 리다이렉트
                    res.json(false);
                }
            }
        } catch (err) {
            logger.error("JWT Controller Error")
            console.log(err);
        }
    }
}
  • 리프레시 토큰이 없거나 유효하지 않은 경우  > 로그인 페이지로 Redirect 시킨다.
  • 리프레시 토큰이 유효할 경우 > getParsing으로 토큰에서 가져온 회원 정보를 payload로 담아 accessToken 생성한다.

 


 

여기서부터는 로그인 시 발급받았던 토큰을 클라이언트에 저장하고,

인증이 필요한 요청에 클라이언트가 토큰을 전달하면 verify() 함수를 사용해 토큰이 유효한지 체크할 것이다.

 

인증 미들웨어 설정

  • 인증이 필요한 API로 접근 시 JWT 토큰 정보가 있고 유효한 지를 먼저 체크하는 미들웨어를 생성할 것이다.
  • HTTP 헤더에 authorization 키가 있는지 체크하고 있다면, 토큰 정보가 Baerer 다음에 들어가 있으므로 Bearer가 있는지 체크하고 없다면 에러(401, 403, 405)를 return 한다.
  • 토큰을 꺼내서 verify 함수를 사용해서 토큰이 유효한지 체크한다.
    • req에 userid와 pw 정보를 세팅하여 뒤에 나올 권한 미들웨어에 넘겨주면 된다.

※ JTW 참고 : 2022.07.11 - [Spring] - JWT(Json Web Token) 발행 및 확인

 

JWT(Json Web Token)발행 및 확인

※ 예약 시스템을 분석하다가 JWT를 사용하는 것을 보고 궁금해서 찾아보는 계기가 되었습니다. JWT에 대해 아주 깔끔하게 정리된 포스팅이 있어서 그대로 가져와서 정리해 두었습니다. 참고한

jjuun93.tistory.com

 

login후 userList에 인증 미들웨어 적용

  • 인증이 필요한 API에 인증 미들웨어를 적용한다.
  • 로그인과 회원가입은 인증이 필요하지 않다.
  • /userList 경로에는 인증이 필요하므로 인증 미들웨어를 설정한다.
router.post('login', LoginController.login);
router.post('userList', jwt_middleware.tokenVerify, UserController.userList);

module.exports = router;

 

  • config 폴더를 만든 후 jwt_middleware.js 파일을 생성해 주었다.

jwt 구성

▶ 참고로 아래 next() 메서드는 현재 라우터에서 판단하지 않고 다음 라우터에서 response를 처리하겠다는 의미이다.

 

 jwt_middleware.js

const e = require("express");
const express = require("express");
const jwt = require('../config/jwt');
const logger = require('../config/logger')


exports.tokenVerify = async (req, res, next) => {

    if (req.get('authorization') !== undefined) {

        const req_token = req.get('authorization');
        const access_token = req_token.substring(7);

        if (access_token !== 'null') {
        //이부분을 주로 보면 되겠습니다.
            const auth_jwt = await jwt.token().getTokenChk(access_token, 'access');
            if (auth_jwt) {
                logger.info("Access Token available");
                next();
            } else {
                logger.warn("Access Token expiration");
                res.json(403, { message: "Access Token expiration" });
            }
        } else {
            logger.warn("Access Token none");
            res.json(403, { message: 'Access Token none' });
        }
    } else {
        logger.warn("Access Token undefined");
        res.json(405, { message: 'No permission' });
    }

}

 

 jwt.js - 위에서 살펴보았던 코드 (jetTokenChk() 함수)

require("dotenv").config();
const jwt = require('jsonwebtoken');
const logger = require("./logger");

exports.token = () => {
    return {
        ...
        async getTokenChk(token, value) {
            try {
                let secret;
                if (value === 'access') {
                    secret = process.env.ACCESS_TOKEN_SECRET
                } else {
                    secret = process.env.REFRESH_TOKEN_SECRET
                }
                const tokenVal = await jwt.verify(token, secret);


                return true;
            } catch (err) {
                logger.error(err);
                logger.error("Token Verify Error");
                return false;
            }

        }
    }
}

 jwt.verify(token, secretOrPublicKey, [options, callback])

  • token - jwt.sign() 메서드로 생성된 JWT입니다.
  • secretOrPublicKey - 서명을 만들 때 사용된 암호 문자열이다.(따로 env파일에 설정해 두며 숨겨둔다.)
  • callback - JWT가 검증된 후 호출되는 function 이다. (payload 값(decode)이나 에러(err)를 인자 사용)

 

  1. verify 함수를 사용해서 토큰이 유효한지 체크하고 유효하면 true, 유효하지 않으면 false return 해준다.
  2. 이후 return 받은 값으로 jwt_middleware.js에서 조건을 판단하여 성공 로직 또는 에러 코드 전달한다.

 

마무리

JWT를 활용한 로그인 구현을 해보면서, 장단점이 있지만 왜 JWT를 활용하는지 알 거 같다.

일단, 기본적으로 Session은 서버에서 유저의 Session을 계속 유지해줘야 하지만 JWT는 유저가 가지고 있고 서버에선 검증만 하면 되니 자원관리의 측면에서 훨씬 좋다.

반응형
  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유