📦src
┣ 📂config
┃ ┗ 📜database.js
┣ 📂controllers
┃ ┗ 📜authController.js
┣ 📂middlewares
┃ ┗ 📜authMiddleware.js
┣ 📂routes
┃ ┗ 📜auth.js
┗ 📂services
┗ ┗ 📜authService.js
이런 구조의 백엔드가 있다고 가정해보자.
백엔드는 프론트엔드로부터 요청을 받으면.. 아래 순서로 기능이 동작하게 된다.
routes -> middlewares -> controllers -> services(config)
routes는 요청 URL에 따라서 어느 기능이 호출되야 하는지를 제어.
middlewares는 기능 호출 이전에 수행되어야 할 로직이 있다면 이를 처리.
(여기에서는 JWT 검증)
controllers는 DB 접근에 필요한 요소들을 정리해서 services로 보낼 준비를 하고, services의 결과를 받아서 적절한 형태로 가공, 에러 처리 등을 수행.
services는 DB에 쿼리문을 전송하고, 반환값을 받아서 처리.
const jwt = require('jsonwebtoken');
const { StatusCodes } = require('http-status-codes');
const authService = require('../services/authService');
exports.verifyToken = async (req, res, next) => {
try {
const token = req.cookies?.authToken || req.headers.authorization?.split(' ')[1];
if (!token) {
return res.status(StatusCodes.UNAUTHORIZED).json({ error: '인증 토큰이 필요합니다.' });
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET_KEY);
req.userId = decoded.id; // 디코딩된 사용자 ID를 요청 객체에 추가
return next();
} catch (error) {
if (error.name === 'TokenExpiredError') {
// 리프레시 토큰 검증 및 재발급
const refreshToken = req.body.refreshToken || req.cookies?.refreshToken;
if (!refreshToken) {
return res.status(StatusCodes.UNAUTHORIZED).json({ error: '리프레시 토큰이 필요합니다.' });
}
try {
const newAccessToken = await authService.verifyRefreshTokenAndGenerateAccessToken(refreshToken);
res.cookie('authToken', newAccessToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
maxAge: 15 * 60 * 1000, // 15분
});
req.userId = jwt.decode(newAccessToken).id;
return next();
} catch (refreshError) {
return res.status(StatusCodes.UNAUTHORIZED).json({ error: '리프레시 토큰 검증 실패' });
}
} else {
return res.status(StatusCodes.UNAUTHORIZED).json({ error: '유효하지 않은 인증 토큰입니다.' });
}
}
} catch (generalError) {
return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ error: '서버 에러가 발생했습니다.' });
}
};
exports.refreshToken = async (req, res) => {
try {
const { refreshToken } = req.body;
if (!refreshToken) {
return res.status(StatusCodes.BAD_REQUEST).json({ error: '리프레시 토큰이 필요합니다.' });
}
// 서비스 호출
const newAccessToken = await authService.verifyRefreshTokenAndGenerateAccessToken(refreshToken);
// 새로운 액세스 토큰을 쿠키에 설정
res.cookie('authToken', newAccessToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
maxAge: 15 * 60 * 1000, // 15분
});
return res.status(StatusCodes.OK).json({ accessToken: newAccessToken });
} catch (error) {
return res.status(StatusCodes.UNAUTHORIZED).json({ error: error.message });
}
};
exports.verifyRefreshTokenAndGenerateAccessToken = async (refreshToken) => {
try {
// 리프레시 토큰 검증
const decoded = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET_KEY);
// 데이터베이스에서 리프레시 토큰 유효성 확인
const query = 'SELECT * FROM users WHERE id = ? AND refresh_token = ?';
const [rows] = await db.execute(query, [decoded.id, refreshToken]);
if (rows.length === 0) {
throw new Error('유효하지 않은 리프레시 토큰입니다.');
}
// 새로운 액세스 토큰 생성
return jwt.sign({ id: decoded.id }, process.env.JWT_SECRET_KEY, { expiresIn: '15m' });
} catch (error) {
if (error.name === 'TokenExpiredError') {
throw new Error('리프레시 토큰이 만료되었습니다. 다시 로그인하십시오.');
}
throw new Error('리프레시 토큰 검증 실패');
}
};
이전 포스팅에 설명했던 내용이지만..
액세스 토큰: 사용자 인증 정보를 담은 짧은 만료 시간의 JWT. 빠른 검증과 요청 보호를 위해 사용.
리프레시 토큰: 더 긴 만료 시간을 가지며, 만료된 액세스 토큰을 갱신하기 위해 사용.
V
V
V