2022.06.02 - [NodeJS] - [NodeJS] Express와 Sequelize-cli를 사용한 로그인 구현
[NodeJS] Express와 Sequelize-cli를 사용한 로그인 구현
이전 글, 회원가입 구현 2022.06.02 - [NodeJS] - [NodeJS] Express와 Sequelize-cli를 사용한 회원가입 구현 [NodeJS] Express와 Sequelize-cli를 사용한 회원가입 구현 [NodeJS] Express와 Sequelize-cli를 사용..
gguzuck.tistory.com
이전 글에서 로그인 까지 구현했으니
이번 차례에는 첫 로그인을 하면 토큰을 발행하고,
그 토큰을 브라우저 쿠키에 저장하여 자동 로그인에 사용하는 JWT 방식,
거기서 더욱 발전한
Access 토큰 과 Refresh 토큰,
토큰 두개를 사용한 방식을 제작해본다.
일단 기본적으로 JWT 가 구현되는 방식인데,
1. 로그인 한다
2. 로그인이 성공한다면 그에 맞는 토큰을 발급한다.
3. 토큰을 브라우저 쿠키에 저장한다.
4. 이후 로그인을 진행하면, 쿠키에 저장된 쿠키를 확인한다.
5. 쿠키가 유효하다면, 자동 로그인을 진행한다.
라는 식으로 진행된다.
하지만 이렇게 진행하게 되면 문제점이 생기는데,
쿠키의 문제점은 CSRF 공격에 취약하다.
CSRF 공격이란
사용자 인증이 되어 있을경우,
그 인증을 가지고 악의적인 공격을 하는 수법을 뜻한다.
즉, 쿠키에 저장된 토큰을 통해 인증된 상태로 유지된다면
CSRF 공격에 그대로 노출된다는것이다.
따라서 이를 해결하기 위해서는 쿠키의 유지시간을 줄여야하는데,
이는 자동로그인을 하는데에 문제가 되고,
이러지도 저러지도 못하는 상태가 된다.
이를 해결하기 위해,
토큰을 두개 발급하는 형식을 취한다.
원래 자동로그인, 즉, 인증에 사용하는 토큰은 Access 토큰이라고 명칭하고,
Refresh 토큰 이라는, 본 서버와 통신할때에만 사용하는, 인증을 위한 추가 인증 토큰을 만든다.
그리고 Access 토큰은 만료 시간을 줄여, 본 서버에서 사용한 후에는 사용할 시간을 줄여버리고,
Refresh 토큰은 만료 기간을 원래대로 길게 만들어두고, 본 서버를 사용할때 Access 토큰을 발급하는 용도로만 사용한다.
순서를 따지면 이런식으로 변하게 된다.
1. 첫 로그인을 진행한다.
2. Access 및 Refresh 토큰을 발행하여, 사용자의 쿠키에 저장한다.
3. 이후 로그인을 진행한다.
4. 먼저 쿠키에 Access 토큰이 있는지 확인한다.
4-1. Access 토큰이 있다면 자동 로그인을 진행한다.
4-2. Access 토큰이 없다면 5번으로 넘어간다.
5. 쿠키에 Refresh 토큰이 있는지 확인한다.
5-1. Refresh 토큰이 있다면 Access 토큰을 발급하고, 자동 로그인을 진행한다.
5-2. Refresh 토큰이 없다면 1번으로 돌아간다.
결국 요약하자면,
토큰을 두개 발급해서 CSRF 공격을 막는다.
라는 이야기다.
이전 글에서도 밝혔듯,
일반 JWT 방식, 즉, 토큰 하나를 사용한 방식은
이미 하는법도 알고, 관련 코드가 사라진 탓에,
이 글에서는 Access 및 Refresh 토큰을 사용한 코드를 적을 생각이다.
// routes/users.js
require("dotenv").config();
const express = require('express');
const cookieParser = require('cookie-parser');
const models = require("../models");
const crypto = require('crypto');
const { appendFile } = require('fs');
const router = express.Router();
const jwt = require('jsonwebtoken');
const bodyParser = require('body-parser');
const app = express();
router.use(cookieParser());
// access token을 secret key 기반으로 생성
const generateAccessToken = (id) => {
return jwt.sign({
id // generateAccessToken(id)
},
process.env.ACCESS_TOKEN_SECRET, // env 내의 access 값
{
expiresIn: '30m' // 30분만 저장
})
}
// refersh token을 secret key 기반으로 생성
const generateRefreshToken = (id) => {
return jwt.sign({
id // generateRefreshToken(id)
},
process.env.REFRESH_TOKEN_SECRET, // env 내의 refresh 값
{
expiresIn: '180 days' // 3달 저장
})
}
// 1 . ACCESS 만료 REFRESH 만료 => users/login
// 2 . ACCESS 만료 REFRESH 유효 => Access 재발급 => users
// 3 . ACCESS 유효 REFRESH 유효 => Refresh 재발급 => users
// 4 . ACCESS 유효 REFRESH 유효 => users
// 자동로그인 Access Token 유효성 검사
const f_authenticateToken = (req, res) =>{
let id = req.body.userEmail;
let Access = req.cookies.OberUser_Access;
let Refresh = req.cookies.OberUser_Refresh;
// Access 토큰이 없다면,
if(!Access){
console.log("Access 토큰이 없습니다.");
// 토큰 둘 다 없는 경우,
if(!Refresh){
console.log("Access 및 Refresh 토큰이 없습니다.");
res.redirect("/users/login");
}
// Access 토큰은 없지만, Refresh는 있는 경우.
else{
console.log("Access 토큰은 없지만, Refresh 토큰은 존재합니다.");
console.log("Access 토큰을 재발급 합니다.");
let AccessToken = generateAccessToken(id);
// AccessToken 쿠키 저장
res.cookie("OberUser_Access", AccessToken , {
expires: new Date(Date.now() + 10800000), // 3시간 저장
httpOnly: true // XSS 공격 대처방안.
});
// 하나라도 토큰이 없을때 발급 완료
// 토큰이 전부 재발급되었으니, 인증 및 로그인 절차
console.log("Access 토큰이 존재합니다.");
res.redirect("/users");
}
}// Refresh 토큰이 없다면.
else if(!Refresh){
// Access 토큰은 있지만, Refresh 토큰이 없는 경우
console.log("Refresh 토큰은 없지만, Access 토큰은 존재합니다.");
console.log("Refresh 토큰을 재발급 합니다.");
let RefreshToken = generateRefreshToken(id);
// RefreshToken 쿠키 저장
res.cookie("OberUser_Refresh", RefreshToken , {
expires: new Date(Date.now() + 5184000000), // 3달 저장
httpOnly: true // XSS 공격 대처방안.
});
}
};
// Access Token 유효성 검사
const authenticateAccessToken = (req, res) =>{
let Access = req.cookies.OberUser_Access;
// 하나라도 토큰이 없을때 발급 완료
// 토큰이 전부 재발급되었으니, 인증 및 로그인 절차
let decode = jwt.verify(Access, process.env.ACCESS_TOKEN_SECRET);
if(decode){
console.log("Access 토큰이 존재합니다.");
res.redirect("/users");
}
};
//###############################################################################
// users(.js)/sign_up 접속시 get.
router.get('/sign_up', function(req, res, next) {
res.render("user/signup");
});
// users(.js)/sign_up 접속시 POST 출력값.
router.post("/sign_up", async function(req,res,next){
let body = req.body;
// inputPassword(DB에 저장할 값) 을 패스워드와 salt를 합치고
// hashPassword에 sha512를 사용하여 제작.
let inputPassword = body.password;
let salt = Math.round((new Date().valueOf() * Math.random())) + "";
let hashPassword = crypto.createHash("sha512").update(inputPassword + salt).digest("base64");
let result = models.user.create({
name: body.userName,
email: body.userEmail,
password: hashPassword,
salt: salt
})
.then( result => {
console.log("성공");
res.redirect("/users/login");
})
.catch( err => {
console.log("실패");
console.log("/users/sign_up");
})
})
//###############################################################################
// 임시 로그인 후 메인 페이지
router.get('/', function(req, res, next) {
if(req.cookies){
console.log(req.cookies);
}
res.send('환영합니다~');
});
//###############################################################################
// 로그인 JWT 테스트 페이지 GET
router.get('/f_login', function(req, res, next) {
res.render("user/f_login");
})
// 로그인 JWT 테스트 페이지 POST
router.post('/f_login', function(req, res, next) {
f_authenticateToken(req, res);
authenticateAccessToken(req, res);
});
//###############################################################################
// 로그인 GET
router.get('/login', function(req, res, next) {
res.render("user/login");
let body = req.body;
});
// 로그인 POST
router.post("/login", async function(req,res,next){
let body = req.body;
let result = await models.user.findOne({
where: {
email : body.userEmail
}
});
let id = req.body.userEmail;
let dbPassword = result.dataValues.password;
let inputPassword = body.password;
let salt = result.dataValues.salt;
let hashPassword = crypto.createHash("sha512").update(inputPassword + salt).digest("base64");
// 쿠키 인증이 안됬고, 패스워드로 인증하기.
if(dbPassword === hashPassword){
console.log("비밀번호 일치");
console.log("비밀번호로 토큰 발급합니다...");
// 패스워드 인증 성공시에 Access 및 Refresh 토큰 발급
let AccessToken = generateAccessToken(id);
let RefreshToken = generateRefreshToken(id);
// AccessToken 쿠키 저장
res.cookie("OberUser_Access", AccessToken , {
expires: new Date(Date.now() + 1800000), // 30분 저장
httpOnly: true // XSS 공격 대처방안.
});
// RefreshToken 쿠키 저장
res.cookie("OberUser_Refresh", RefreshToken , {
expires: new Date(Date.now() + 5184000000), // 3달 저장
httpOnly: true // XSS 공격 대처방안.
});
res.redirect("/users");
}
else{
console.log("비밀번호 불일치");
res.redirect("/users/login");
}
});
module.exports = router;
각 중요한 줄이나 궁금할만한 줄에는
주석처리로 설명을 적어두었다.
'웹 > NodeJS' 카테고리의 다른 글
[NodeJS] Express와 Sequelize-cli를 사용한 로그인 구현 (0) | 2022.06.02 |
---|---|
[NodeJS] Express와 Sequelize-cli를 사용한 회원가입 구현 (0) | 2022.06.02 |
[NodeJS] Express와 Sequelize-cli를 사용한 DB 연결 (0) | 2022.06.02 |
[NodeJS] JWT 사용 이유와 VScode로 JWT 기초를 활용한 토큰 생성 및 검증 해보기 (0) | 2022.05.08 |
[NodeJS] AWS 서버 구축 해보기 (0) | 2022.04.17 |
댓글