참고자료: 견고한 node.js 프로젝트 설계하기
참고자료: 쉽게 알아보는 서버 인증 1편(세션/쿠키, JWT)
참고자료: Express에서 JWT로 인증시스템 구현하기 (Access Token과 Refresh Token)
참고자료: node.js :: Bcrypt로 비밀번호 해싱(Hashing)하기
해당 참고자료들을 참고하여 코드 및 글을 작성했음을 밝힙니다 :)
이번 글에서는 JWT를 활용해 구현한 회원가입/로그인을 정리할 예정!
아마 다음 글에서 토큰 Refresh와 로그아웃 관련한 정리를 할 것 같다.
저번 글에서 Router에 관한 이야기를 했으니, 오늘 글의 순서는
가 될 듯!
JWT_SECRET_ACCESS_KEY=###########
JWT_SECRET_REFRESH_KEY=############
JWT_ALGORITHM=HS256
JWT_ACCESS_EXPIRE=1h
JWT_REFRESH_EXPIRE=14d
우선 .env 파일에 다음과 같이 AccessToken, RefreshToken 발급 시에 사용할 secret key와 알고리즘, 만료 시간을 작성해 주었다!
const jwt = require('jsonwebtoken');
const { User } = require('../models');
require('dotenv').config();
exports.signAccess = (userData) => { //AccessToken 발급
const payload = {
pk:userData.pk,
email:userData.email
}
return jwt.sign(
payload,
process.env.JWT_SECRET_ACCESS_KEY,
{
algorithm:process.env.JWT_ALGORITHM,
expiresIn:process.env.JWT_ACCESS_EXPIRE
}
);
};
exports.signRefresh = (userPk) => { //RefreshToken 발급
return jwt.sign(
{pk:userPk},
process.env.JWT_SECRET_REFRESH_KEY,
{
algorithm:process.env.JWT_ALGORITHM,
expiresIn:process.env.JWT_REFRESH_EXPIRE,
}
);
}
exports.verifyAccess = (accessToken) => { //AccessToken 검증
try {
const verified = jwt.verify(accessToken, process.env.JWT_SECRET_ACCESS_KEY)
return {
success:true,
message:'Token Verified',
userData: {
pk:verified.pk,
email:verified.email
}
}
} catch(error) {
return {
success:false,
message:error.message,
userData:null
}
}
}
exports.verifyRefresh = async (refreshToken) => { //RefreshToken 검증
try {
const verified = jwt.verify(refreshToken, process.env.JWT_SECRET_REFRESH_KEY);
const user = await User.findOne({where:{pk:verified.pk}});
const userToken = user.dataValues.Refresh;
if (userToken === refreshToken) {
return {
success:true,
message:'Token exists and verified',
userPk:verified.pk
};
};
if (userToken !== refreshToken) {
return {
success:false,
message:'Token valid, but not found in DB',
userPk:verified.pk
};
};
} catch (error) {
return {
success:false,
message:'Token not verified',
userPk:null
};
};
};
exports.getAccess = ({authorization}) => {
try {
const accessToken = authorization.split('Bearer ')[1];
return accessToken
} catch(error) {
return error.message;
}
}
exports.getUserWithAccess = async (accessToken) => {
try {
const verified = jwt.verify(accessToken, process.env.JWT_SECRET_ACCESS_KEY)
const user = await User.findOne({where:{pk:verified.pk}})
return user.dataValues
} catch(error) {
return error.message;
}
}
exports.getUserWithRefresh = async (refreshToken) => {
try {
const verified = jwt.verify(refreshToken, process.env.JWT_SECRET_REFRESH_KEY);
const user = await User.findOne({where:{pk:verified.pk}})
return user.dataValues;
} catch(error) {
return error.message;
}
}
RefreshToken에는 원래 사용자 식별 정보를 담지 않았는데, Token 갱신 시에 RefreshToken에서도 사용자 정보를 추출할 수 있어야 할 것 같아 pk값만 넣는 것으로 수정해 둔 상태!
authorization
이라는 키에 담겨 Bearer {accessToken}
이라는 형태로 전달되는데, 해당 부분에서 accessToken 부분만을 추출하는 함수const authServices = require('../services/authServices');
exports.signUp = async(req, res, next) => {
try {
const context = await authServices.signUp(req.body);
if (context['user']) {
next(); //router에서 다음 -> 로그인 로직으로.
} else {
res.status(409).json({
msg:context['msg']
});
}
} catch (error) {
console.log(error);
res.status(400).json({
msg:context['msg']
});
}
};
exports.logIn = async(req, res, next) => {
try {
const authUser = await authServices.logIn(req.body);
res.status(200).json({
msg:'로그인 성공',
data:authUser
});
} catch(error) {
console.log(error);
next(error);
}
};
견고한 node.js 프로젝트 설계하기 참고자료를 다 이해하진 못했지만..
을 수행하는 느낌으로 코드를 작성해 보았다.
signUp
함수는 회원가입 이후 로그인까지 이어질 수 있도록 작성한 것 외에는..
특별한 부분은 없는 듯!
const bcrypt = require('bcrypt');
const {User} = require('../models');
require('dotenv').config();
const {signAccess, signRefresh, verifyAccess, verifyRefresh, getUserWithRefresh} = require('../utils/auth');
exports.signUp = async ({nickname, email, password}) => {
let context = {'user':null, 'msg':''};
try {
const userExists = await User.findOne({where:{email}}) //없으면 null 반환
if (userExists) {
context['msg'] = '이미 사용 중인 이메일입니다!'
console.log('이미 사용 중인 이메일입니다!');
return context;
} else {
const salt = await bcrypt.genSalt(10);
const hashed_pw = await bcrypt.hash(password, salt);
const user = await User.create({
nickname,
email,
password:hashed_pw
});
context['user'] = user;
return context;
}
} catch(error) {
console.log(error);
context['msg'] = 'catch' + error.message;
return context;
}
};
exports.logIn = async({email, password}) => {
const user = await User.findOne({where:{email}})
const userData = user.dataValues;
const accessToken = signAccess(userData);
const refreshToken = signRefresh(userData.pk);
const tokens = {
access:accessToken,
refresh:refreshToken,
};
user.Refresh = refreshToken;
user.save();
return {userData, tokens};
}
실제 비즈니스 로직에 해당하는 services는 위와 같이 작성해 보았다.
signUp
에서는
회원가입 시에 req.body
에 담아서 보내는 nickname, email, password 정보를 받아
해당 이메일을 사용 중인 사용자가 있는지를 확인하고, 있다면
회원가입이 정상적으로 되지 않은 상태이기에
{
'user':null,
'msg':'이미 사용 중인 이메일입니다!'
}
라는 context를 반환한다.
반환값을 context라는 object에 담는 방식은 예전에 Django로 백엔드 로직을 작성할 때 본 적이 있는 방식이라 그대로 사용했는데.. 통상적인 방식인지는 모르겠음...
백엔드 내에서만 사용되는 값이라 그냥 나 편한 대로 작성해따..
이메일이 이미 사용 중이지 않다면 bcrypt
라이브러리를 사용해서
salt를 생성하고, 비밀번호를 해싱하는 과정을 거쳐 사용자 정보를 DB에 생성하고, 해당 사용자 정보를 반환해 준다.
bcrypt
라이브러리에 대해서는 참고자료에 잘 정리해 주신 분의 글을 링크해 두어, 참고하면 좋을 듯!
간단히 이야기하면,
Hashing
이란
그리고 그 과정에서 사용되는 salt
란, 음식에 살짝 소금을 더하듯이
salt
라는 임의의 값을 추가해서 Hashing
함을 통해, logIn
에서는 req.body
에 담아서 보내는 사용자의 이메일, 비밀번호를 받아 해당하는 사용자를 찾고, 사용자 정보를 담아 accessToken
과 refreshToken
을 발급해 준다.
passport 단에서 검증을 마친 유효한 사용자 정보만 들어오기 때문에, login에서 따로 이메일, 비밀번호의 유효성을 검증하지는 않았다.
사용자의 refreshToken
은 사용자 정보에 함께 담아 DB에 저장하고,
사용자 정보와, tokens
를 다시 controller에 반환해 준다.
#2. 에서 작성했듯, services에서 반환해 준 값을 controller가 받아,
반환값에 따라 적절한 응답을 보내주면 마무리 :)
코드를 작성할 때 분명 여러 번 찾아보았던 내용들이지만, 글로 정리하면서 한 번씩 더 찾아보고, 복습할 수 있게 되는 것 같아 즐겁게 작성 중이다 ㅎㅎ