반응형
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 식별자이다.
- expiresIn(exp) - JWT 만료시간으로 초 단위 또는 시간 범위 문자열로 설정한다.
- callback - JWT가 생성된 후 호출되는 funcation이다. (생성된 토큰(token)이나 에러(err)를 인자로 사용)
중간에 React로 프론트 단에서 설명은 다른 포스팅으로 대체하겠습니다.
2022.08.08 - [Java Script] - 리액트 JWT 토큰과 localStorage를 이용한 로그인
토큰 재발급 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) 발행 및 확인
login후 userList에 인증 미들웨어 적용
- 인증이 필요한 API에 인증 미들웨어를 적용한다.
- 로그인과 회원가입은 인증이 필요하지 않다.
- /userList 경로에는 인증이 필요하므로 인증 미들웨어를 설정한다.
router.post('login', LoginController.login);
router.post('userList', jwt_middleware.tokenVerify, UserController.userList);
module.exports = router;
- config 폴더를 만든 후 jwt_middleware.js 파일을 생성해 주었다.
▶ 참고로 아래 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)를 인자 사용)
- verify 함수를 사용해서 토큰이 유효한지 체크하고 유효하면 true, 유효하지 않으면 false return 해준다.
- 이후 return 받은 값으로 jwt_middleware.js에서 조건을 판단하여 성공 로직 또는 에러 코드 전달한다.
마무리
JWT를 활용한 로그인 구현을 해보면서, 장단점이 있지만 왜 JWT를 활용하는지 알 거 같다.
일단, 기본적으로 Session은 서버에서 유저의 Session을 계속 유지해줘야 하지만 JWT는 유저가 가지고 있고 서버에선 검증만 하면 되니 자원관리의 측면에서 훨씬 좋다.
반응형
'Java' 카테고리의 다른 글
[React] JWT 토큰과 localStorage 로그인 (0) | 2022.08.08 |
---|---|
[JWT 저장] localStorage vs Cookie 비교 (0) | 2022.08.04 |
[JS] localStorage & sessionStorage 비교 (0) | 2022.07.29 |
[JS] JSON 값 넣는 방법 (0) | 2022.07.29 |
[Spring] @Profile, @ActiveProfiles (0) | 2022.07.28 |