Express 백엔드에서 Passport와 jsonwebtoken을 사용하여 인증 처리 시스템을 구현하는 방법을 설명합니다. 주요 개념, 설치 방법, 그리고 실용적인 코드를 포함한 안내서를 제공합니다.
토큰은 mysql 에 저장하는 방식을 취했습니다.
1.백엔드
1. 프로젝트에 필요한 라이브러리 설치
npm install express passport passport-local passport-kakao jsonwebtoken bcrypt dotenv sequelize mysql2 express-session cors cookie-parser morgan
위 명령어를 실행하면, Express와 관련된 주요 인증 및 데이터 처리 라이브러리가 설치됩니다.
2. 프로젝트 구조 설계 및 환경 설정
- 프로젝트 구조
/project ├── /models # Sequelize 모델 정의 ├── /routes # 라우터 파일 ├── /passport # Passport 전략 파일 ├── /utils # 유틸리티 함수 ├── app.js # Express 서버 메인 파일 ├── .env # 환경 변수
.env 예시
JWT_SECRET_KEY=your_secret_key ACCESS_TOKEN_SECRET=your_access_secret REFRESH_TOKEN_SECRET=your_refresh_secret COOKIE_SECRET=your_cookie_secret PORT=8001
models/user.js
const Sequelize = require('sequelize');
const { DataTypes } = require('sequelize');
class User extends Sequelize.Model {
static initiate(sequelize) {
User.init({
id: { // 기본 키로 설정
type: Sequelize.INTEGER,
autoIncrement: true,
primaryKey: true,
},
userId: {
type: Sequelize.STRING(40),
allowNull: false,
unique: true,
},
nick: {
type: Sequelize.STRING(15),
allowNull: false,
},
password: {
type: Sequelize.STRING(100),
allowNull: true,
},
address: {
type: Sequelize.TEXT,
allowNull: false,
},
userType: {
type: DataTypes.ENUM('guest', 'owner'),
allowNull: false,
defaultValue: 'guest',
},
time: {
type: Sequelize.STRING(100),
allowNull : true,
},
}, {
sequelize,
timestamps: true,
underscored: false,
modelName: 'User',
tableName: 'users',
paranoid: true,
charset: 'utf8mb4',
collate: 'utf8mb4_general_ci',
});
}
static associate(db) {
db.User.hasOne(db.RefreshToken, { // User와 RefreshToken 간 1:1 관계
foreignKey: 'userId',
sourceKey: 'userId',
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
});
}
};
module.exports = User;
models/refreshTokens.js
const Sequelize = require('sequelize');
class RefreshToken extends Sequelize.Model {
static initiate(sequelize) {
RefreshToken.init({
id: {
type: Sequelize.INTEGER,
autoIncrement: true,
primaryKey: true,
},
userId: { // User의 기본 키 id와 연결
type: Sequelize.INTEGER, // User.id의 데이터 타입과 일치시킴
allowNull: false,
unique: true, // 한 User는 하나의 RefreshToken만 가질 수 있음
},
refreshToken: {
type: Sequelize.STRING(255),
allowNull: false,
},
expiresAt: {
type: Sequelize.DATE,
allowNull: false,
},
}, {
sequelize,
timestamps: true,
underscored: false,
modelName: 'RefreshToken',
tableName: 'refresh_tokens',
paranoid: false,
charset: 'utf8mb4',
collate: 'utf8mb4_general_ci',
});
}
static associate(db) {
db.RefreshToken.belongsTo(db.User, {
foreignKey: 'userId', // RefreshToken.userId -> User.id
targetKey: 'id', // User의 기본 키(id)에 연결
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
});
}
}
module.exports = RefreshToken;
3. app.js 구현
app.js는 Express 애플리케이션의 메인 엔트리로, 불필요한 코드는 제거하고 중요한 기능에 집중합니다.
const express = require('express');
const cookieParser = require('cookie-parser');
const session = require('express-session');
const dotenv = require('dotenv');
const passport = require('passport');
const cors = require('cors');
const { sequelize } = require('./models');
const passportConfig = require('./passport');
const authRouter = require('./routes/auth'); // 인증 관련 라우터
dotenv.config();
const app = express();
passportConfig(); // Passport 설정 초기화
// 기본 설정
app.set('port', process.env.PORT || 8001);
// DB 연결
sequelize.sync({ force: false })
.then(() => console.log('데이터베이스 연결 성공'))
.catch(err => console.error(err));
// 미들웨어 설정
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser(process.env.COOKIE_SECRET));
app.use(session({
resave: false,
saveUninitialized: false,
secret: process.env.COOKIE_SECRET,
cookie: { httpOnly: true, secure: false },
}));
app.use(passport.initialize());
app.use(passport.session());
app.use(cors({
origin: 'http://localhost:3000', // React 프론트엔드 도메인
credentials: true,
}));
// 라우터 연결
app.use('/api/auth', authRouter);
// 에러 처리
app.use((req, res, next) => {
const error = new Error('Not Found');
error.status = 404;
next(error);
});
app.use((err, req, res, next) => {
res.status(err.status || 500).json({ message: err.message });
});
// 서버 시작
app.listen(app.get('port'), () => {
console.log(`Server running on port ${app.get('port')}`);
});
4. Passport 설정 (passport/index.js)
Passport는 인증 요청 처리의 핵심 역할을 합니다. local과 kakao 전략을 통합 관리합니다
const passport = require('passport');
const local = require('./localStrategy');
const kakao = require('./kakaoStrategy');
const User = require('../models/user');
module.exports = () => {
passport.serializeUser((user, done) => {
console.log('serialize');
done(null, user.id);
});
passport.deserializeUser((id, done) => {
console.log('deserialize');
User.findOne({
where: { id },
include: [{
model: User,
attributes: ['id', 'nick'],
as: 'Followers',
}, {
model: User,
attributes: ['id', 'nick'],
as: 'Followings',
}],
})
.then(user => {
console.log('user', user);
done(null, user);
})
.catch(err => done(err));
});
local();
kakao();
};
localStrategy.js
로컬 로그인 전략 (passport/localStrategy.js)
const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
const bcrypt = require('bcrypt');
const User = require('../models/user');
module.exports = () => { // 로그인
passport.use(new LocalStrategy({
usernameField: 'userId', // req.body.userId
passwordField: 'password', // req.body.password
passReqToCallback: false,
}, async (userId, password, done) => {
try {
const exUser = await User.findOne({ where: { userId } });
if (exUser) {
const result = await bcrypt.compare(password, exUser.password);
if (result) {
done(null, exUser);
} else {
done(null, false, { message: '비밀번호가 일치하지 않습니다.' });
}
} else {
done(null, false, { message: '가입되지 않은 회원입니다.' });
}
} catch (error) {
console.error(error);
done(error);
}
}));
};
kakaoStrategy.js
const passport = require('passport');
const KakaoStrategy = require('passport-kakao').Strategy;
const User = require('../models/user');
module.exports = () => {
passport.use(new KakaoStrategy({
clientID: process.env.KAKAO_ID,
callbackURL: '/auth/kakao/callback',
}, async (accessToken, refreshToken, profile, done) => {
console.log('kakao profile', profile);
try {
const exUser = await User.findOne({
where: { snsId: profile.id, provider: 'kakao' },
});
if (exUser) {
done(null, exUser);
} else {
const newUser = await User.create({
email: profile._json?.kakao_account?.email,
nick: profile.displayName,
snsId: profile.id,
provider: 'kakao',
});
done(null, newUser);
}
} catch (error) {
console.error(error);
done(error);
}
}));
};
5. JWT 유틸리티 (utils/auth.js)
JWT 토큰 생성 및 검증을 담당하는 유틸리티 함수를 정의합니다.
const { sign, verify, decode } = require('jsonwebtoken');
const { compare } = require('bcrypt');
const RefreshToken = require('../models/refreshTokens'); // Sequelize 모델 임포트
const User = require('../models/user'); // User 모델 임포트 (User 모델이 있다고 가정)
const { NotAuthError } = require('./errors');
// 암호화 키를 설정합니다.
const KEY = process.env.JWT_SECRET_KEY;
const ACCESS_TOKEN_SECRET = process.env.ACCESS_TOKEN_SECRET;
const REFRESH_TOKEN_SECRET = process.env.REFRESH_TOKEN_SECRET;
const ACCESS_TOKEN_EXPIRATION = '1h'; // 1시간
const REFRESH_TOKEN_EXPIRATION = 14 * 24 * 60 * 60; // 14일 (초 단위)
// 1. 입력받은 비밀번호와 저장된 비밀번호를 비교하는 함수
function isValidPassword(password, storedPassword) {
return compare(password, storedPassword);
}
// 2. 접근 토큰 생성
function createAccessToken(userId, userType) {
console.log("2. 접근 토큰 생성 :", userId,userType, ACCESS_TOKEN_SECRET, ACCESS_TOKEN_EXPIRATION);
const accessToken = sign({ userId,userType }, ACCESS_TOKEN_SECRET, { expiresIn: ACCESS_TOKEN_EXPIRATION });
const decodedToken = decode(accessToken);
return {
accessToken,
accessTokenExpires: decodedToken.exp,
};
}
// 3. 갱신 토큰 생성
function createRefreshToken(userId,userType) {
const refreshToken = sign({ userId ,userType}, REFRESH_TOKEN_SECRET, { expiresIn: REFRESH_TOKEN_EXPIRATION });
const decodedToken = decode(refreshToken);
return {
refreshToken,
refreshTokenExpires: decodedToken.exp,
};
}
// 4. 접근 토큰 유효성 검사
function validateAccessToken(token) {
return verify(token, ACCESS_TOKEN_SECRET);
}
// 5. 갱신 토큰 유효성 검사
function validateRefreshToken(token) {
return verify(token, REFRESH_TOKEN_SECRET);
}
// 6. 갱신 토큰을 MySQL에 저장하는 함수
async function storeRefreshToken(refreshToken, userId) {
try {
const expiresAt = new Date(Date.now() + REFRESH_TOKEN_EXPIRATION * 1000); // 만료 시간 계산
const existingToken = await RefreshToken.findOne({ where: { userId } });
if (existingToken) {
// 기존 토큰 업데이트
existingToken.refreshToken = refreshToken;
existingToken.expiresAt = expiresAt;
await existingToken.save();
} else {
// 새 토큰 생성
const newToken = await RefreshToken.create({
userId: userId,
refreshToken,
expiresAt,
});
}
console.log("6. 갱신 토큰을 MySQL에 저장하는 함수 :", userId, refreshToken, expiresAt);
} catch (error) {
console.error('Failed to store refresh token:', error);
throw new Error('Failed to store refresh token');
}
}
// 7. 저장된 MySQL에서 갱신 토큰 가져오는 함수
async function getStoredRefreshToken(userId) {
try {
const tokenData = await RefreshToken.findOne({ where: { userId } });
return tokenData ? tokenData.refreshToken : null;
} catch (error) {
console.error('Failed to retrieve refresh token:', error);
throw new Error('Failed to retrieve refresh token');
}
}
// 8. 인증 미들웨어 함수
async function checkAuthMiddleware(req, res, next) {
console.log("미들웨어 인증 처리 :");
if (req.method === 'OPTIONS') {
return next();
}
if (!req.headers.authorization) {
console.log("접근 권한이 없습니다.-1 :");
return next(new NotAuthError('접근 권한이 없습니다.'));
}
const authFragments = req.headers.authorization.split(' ');
if (authFragments.length !== 2) {
console.log("not-authenticated-2 :");
return next(new NotAuthError('접근 권한이 없습니다.'));
}
const authToken = authFragments[1];
try {
const validatedToken = validateAccessToken(authToken);
req.token = validatedToken;
req.userId = validatedToken.userId;
req.user = {}; // req.user 객체 초기화
req.user.userType = validatedToken.userType;
req.userType = validatedToken.userType;
} catch (error) {
console.log("not-authenticated-3:", error.message);
if (error.message === 'jwt expired') {
console.log("접근 토큰 인증 만료");
return res.status(403).json({ message: 'ACCESS_TOKEN_EXPIRED' });
}
return next(new NotAuthError('접근 권한이 없습니다.'));
}
next();
}
// 9. 로그아웃 시 갱신 토큰 삭제
async function deleteRefreshToken(userId) {
try {
await RefreshToken.destroy({ where: { userId } });
console.log(`갱신 토큰 삭제 완료: ${userId}`);
} catch (error) {
console.error('Failed to delete refresh token:', error);
throw new Error('Failed to delete refresh token');
}
}
// 10. 사용자 등록 시 (회원가입)
async function registerUser(userData) {
try {
const { userId, password, nick } = userData;
const userExists = await User.findOne({ where: { userId } });
if (userExists) {
throw new Error('이미 존재하는 아이디입니다.');
}
// 비밀번호 암호화 후 사용자 생성
const hashedPassword = await bcrypt.hash(password, 10);
const newUser = await User.create({
userId,
password: hashedPassword,
nick, // 추가된 닉네임
});
return newUser;
} catch (error) {
console.error('회원가입 실패:', error);
throw error;
}
}
module.exports = {
createAccessToken,
createRefreshToken,
validateAccessToken,
validateRefreshToken,
isValidPassword,
storeRefreshToken,
getStoredRefreshToken,
checkAuthMiddleware,
deleteRefreshToken,
registerUser, // 회원가입 처리 함수 추가
};
errors.js
class NotAuthError {
constructor(message) {
this.message = message;
this.status = 401; // 인증 실패를 나타내는 HTTP 상태 코드
}
}
exports.NotAuthError = NotAuthError;
6. 인증 컨트롤(routes/auth.js)
/api/auth에서 로그인 및 JWT 토큰 발급을 처리합니다
passport/localStrategy.js 와 연결처리 되어 로그인 인증처리 됩니다.
const bcrypt = require('bcrypt');
const passport = require('passport');
const User = require('../models/user');
const { createJSONToken, isValidPassword, createAccessToken, createRefreshToken, storeRefreshToken, deleteRefreshToken, validateRefreshToken } = require("../util/auth");
// 회원가입!
exports.join = async (req, res, next) => {
//console.log("회원가입 요청 데이터: ", req.body);
const { userId, nickname, password, userType, address } = req.body;
if (!userId || !nickname || !password || !address || !userType) {
return res.status(400).send('모든 필드를 입력해주세요.');
}
try {
console.log(req.body);
const exUser = await User.findOne({ where: { userId } });
// 로그인 - 일단 이 아이디로 가입한 유저가 있는지 찾기
if (exUser) {
return res.status(400).json({ responseMessage: '이미 존재하는 사용자입니다.' });
}
let time=req.body.time;
if(!time) {
time=null;
}
const hash = await bcrypt.hash(password, 12); // bcrypt 비밀번호 암호화
await User.create({
userId,
nick: nickname,
password: hash,
userType,
address: JSON.stringify(address),
time: time
});
//return res.redirect('/');
return res.status(201).json({ responseMessage: '회원가입 성공' });
} catch (error) {
console.error(error);
return next(error);
}
}
// 로그인!
exports.login =async (req, res, next) => {
passport.authenticate('local',async (authError, user, info) => {
if (authError) return res.status(500).json({ responseMessage: '서버 에러가 발생했습니다.' });
if (!user) return res.status(400).json({ responseMessage: info.message });
try {
const userId = user.userId;
const { accessToken: newAccessToken } = createAccessToken(user.id, user.userType);
const { refreshToken: newRefreshToken } = createRefreshToken(user.id, user.userType);
await storeRefreshToken(newRefreshToken, user.id);
let parsedAddress = {};
if (user.address) parsedAddress = JSON.parse(user.address);
return res.status(200).json({
id: user.id,
userId,
nickname: user.nick,
address: parsedAddress,
userType: user.userType,
access_token: newAccessToken,
refresh_token: newRefreshToken
});
} catch (error) {
console.error("로그인 처리 에러:", error);
return res.status(500).json({ responseMessage: '서버 에러가 발생했습니다.' });
}
})(req, res, next);
};
//갱신 토큰 발급처리
exports.refresh = async (req, res, next) => {
const { refreshToken } = req.body;
//console.log("refresh token: " + refreshToken);
if (!refreshToken) {
return res.status(400).json({
responseMessage: 'refresh token 값이 존재하지 않습니다.',
});
}
// Refresh Token 유효성 검사
const refreshTokenValid = validateRefreshToken(refreshToken);
if (!refreshTokenValid) {
return res.status(400).json({
responseMessage: '갱신 토큰값이 유효하지 않습니다.',
});
}
try {
console.log("갱신 토큰 유효합니다.", refreshTokenValid);
// `validateRefreshToken`이 반환한 데이터 구조 확인
const { userId, userType } = refreshTokenValid;
// Access Token 생성
const { accessToken: newAccessToken } = createAccessToken(userId, userType);
// Refresh Token 생성
const { refreshToken: newRefreshToken } = createRefreshToken(userId, userType);
// 기존 Refresh Token 삭제
await deleteRefreshToken(userId);
// 새로운 Refresh Token 저장
await storeRefreshToken(newRefreshToken, userId);
// 성공 응답
return res.status(200).json({
id: userId,
access_token: newAccessToken,
refresh_token: newRefreshToken,
});
} catch (error) {
console.error("갱신 토큰 처리 중 에러:", error);
// 서버 에러 응답
return res.status(500).json({
responseMessage: '서버에서 에러가 발생했습니다. 잠시 후 다시 시도해주세요.',
});
}
};
exports.logout = async(req, res) => {
req.logout(async () => {
const userId = req.query.userId;
const exUser = await User.findOne({ where: { userId } });
//console.log("로그아웃 시 갱신 토큰 삭제",exUser);
await deleteRefreshToken(exUser.id);
return res.status(200).json({responseMessage: 'success'});
});
};
2.프론트엔드
1. React 프로젝트 준비 및 필수 라이브러리 설치
1.1. 프로젝트 초기화
리액트 프로젝트를 생성하려면 create-react-app을 사용합니다.
npx create-react-app macaronics-clone cd macaronics-clone
1.2. 의존성 설치
package.json에 나열된 라이브러리를 프로젝트에 설치합니다.
npm install axios react-daum-postcode react-icons react-query react-router-dom react-select recoil seamless-scroll-polyfill styled-components
1.3. package.json 파일 예시
다음은 package.json의 기본 구조입니다.
{
"name": "macaronics-clone",
"version": "0.1.0",
"private": true,
"dependencies": {
"axios": "^1.4.0",
"react": "^18.2.0",
"react-daum-postcode": "^3.1.3",
"react-dom": "^18.2.0",
"react-icons": "^4.8.0",
"react-query": "^3.39.3",
"react-router-dom": "^6.11.1",
"react-select": "^5.7.3",
"recoil": "^0.7.7",
"seamless-scroll-polyfill": "^2.3.4",
"styled-components": "^6.0.0-rc.1"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test"
}
}
2. Axios 인스턴스 구성
2.1. Axios 인스턴스 설정
백엔드와의 HTTP 통신을 쉽게 하기 위해 Axios 인스턴스를 생성합니다.
import axios from "axios";
// Axios 인스턴스 생성
export const instance = axios.create({
baseURL: process.env.REACT_APP_SERVER_URL || "http://localhost:8001", // 백엔드 주소
withCredentials: true,
headers: {
"Content-Type": "application/json",
},
});
// Request 인터셉터
instance.interceptors.request.use(
(config) => {
const accessToken = localStorage.getItem("access_token");
const refreshToken = localStorage.getItem("refresh_token");
if (accessToken && refreshToken) {
config.headers["Access_token"] = `${accessToken}`;
config.headers["Refresh_token"] = `${refreshToken}`;
}
if (accessToken) {
config.headers.Authorization = `Bearer ${accessToken}`;
}
return config;
},
(error) => Promise.reject(error)
);
// Response 인터셉터
instance.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
if (error.response?.status === 403 && !originalRequest._retry) {
originalRequest._retry = true;
const refreshToken = localStorage.getItem("refresh_token");
if (!refreshToken) {
localStorage.clear();
window.location.href = "/Intro";
return Promise.reject(error);
}
try {
const response = await fetch(
`${process.env.REACT_APP_SERVER_URL}/api/auth/refresh`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ refreshToken }),
}
);
if (response.ok) {
const data = await response.json();
localStorage.setItem("access_token", data.access_token);
localStorage.setItem("refresh_token", data.refresh_token);
return instance(originalRequest);
} else {
localStorage.clear();
window.location.href = "/Intro";
return Promise.reject(error);
}
} catch (err) {
return Promise.reject(err);
}
}
return Promise.reject(error);
}
);
3. 로그인 및 로그아웃 처리
3.1. 로그인 처리
export const login = async (email, password) => {
try {
const response = await instance.post("/auth/login", { email, password });
localStorage.setItem("access_token", response.data.access_token);
localStorage.setItem("refresh_token", response.data.refresh_token);
localStorage.setItem("user", response.data.refresh_token);
return response.data;
} catch (error) {
console.error("로그인 실패:", error);
throw error;
}
};
3.2. 로그아웃 처리
export const userLogout = () => {
const userId=localStorage.getItem("userId");
if(!userId || userId===null){
localStorage.clear();
//alert('로그아웃 성공');
return null;
}
return instance.get(`/api/auth/logout?userId=${userId}`)
.then((response) => {
localStorage.clear();
//alert('로그아웃 성공');
return response;
}).catch((error) => {
localStorage.clear();
if (error.response && error.response.data) {
alert(error.response.data.responseMessage);
} else {
alert('로그아웃 중 에러가 발생했습니다.');
}
return Promise.reject(error);
});
};
4. CRUD 처리 예시
4.1. 데이터 조회
export const getData = async (endpoint) => {
try {
const response = await instance.get(endpoint);
return response.data;
} catch (error) {
console.error("데이터 조회 실패:", error);
throw error;
}
};
4.2. 데이터 생성
export const createData = async (endpoint, payload) => {
try {
const response = await instance.post(endpoint, payload);
return response.data;
} catch (error) {
console.error("데이터 생성 실패:", error);
throw error;
}
};
4.3. 데이터 수정
export const updateData = async (endpoint, payload) => {
try {
const response = await instance.put(endpoint, payload);
return response.data;
} catch (error) {
console.error("데이터 수정 실패:", error);
throw error;
}
};
4.4. 데이터 삭제
export const deleteData = async (endpoint) => {
try {
await instance.delete(endpoint);
console.log("데이터 삭제 성공");
} catch (error) {
console.error("데이터 삭제 실패:", error);
throw error;
}
};
5. 실제 예제: React 컴포넌트와 Axios 통합
5.1. 사용자 리스트 조회
import React, { useEffect, useState } from "react";
import { getData } from "../api/axiosInstance";
const UserList = () => {
const [users, setUsers] = useState([]);
useEffect(() => {
const fetchUsers = async () => {
try {
const data = await getData("/users");
setUsers(data);
} catch (error) {
console.error("사용자 목록 조회 오류:", error);
}
};
fetchUsers();
}, []);
return (
<div>
<h1>사용자 목록</h1>
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
);
};
export default UserList;
위와 같은 방식으로 React에서 Axios를 사용하여 백엔드 API와 통신하는 방법을 구현할 수 있습니다.
interceptors를 활용하면 반복적인 요청/응답 처리를 효율적으로 관리할 수 있습니다.














댓글 ( 0)
댓글 남기기