JWT Authentication System 구현 완벽 가이드 - Node.js 백엔드 중심
Node.js와 Express를 활용한 JWT 인증 시스템의 완전한 구현 방법을 다룹니다. 토큰 생성부터 미들웨어 구현까지 실무 중심으로 설명합니다.
JWT Authentication System 구현 완벽 가이드 - Node.js 백엔드 중심
현대 웹 애플리케이션에서 사용자 인증은 필수적인 요소입니다. JWT(JSON Web Token)는 stateless한 특성과 확장성 때문에 많은 개발자들이 선택하는 인증 방식입니다. 이 글에서는 Node.js와 Express를 활용하여 실무에서 사용할 수 있는 완전한 JWT 인증 시스템을 구현하는 방법을 상세히 알아보겠습니다.
JWT 기본 개념과 구조
JWT는 JSON 객체를 안전하게 전송하기 위한 표준(RFC 7519)입니다. 토큰은 Header, Payload, Signature 세 부분으로 구성되며, 각 부분은 점(.)으로 구분됩니다.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
JWT의 주요 특징은 다음과 같습니다:
- Self-contained: 토큰 자체에 사용자 정보가 포함되어 있음
- Stateless: 서버에서 세션 상태를 관리할 필요가 없음
- Portable: 다양한 플랫폼과 언어에서 사용 가능
- Secure: 디지털 서명을 통한 무결성 보장
프로젝트 설정 및 의존성 설치
먼저 Node.js 프로젝트를 초기화하고 필요한 패키지들을 설치합니다.
mkdir jwt-auth-system
cd jwt-auth-system
npm init -y
npm install express jsonwebtoken bcryptjs mongoose dotenv cors helmet
npm install -D nodemon
기본적인 프로젝트 구조를 설정합니다:
jwt-auth-system/
├── src/
│ ├── controllers/
│ │ └── authController.js
│ ├── middleware/
│ │ └── authMiddleware.js
│ ├── models/
│ │ └── User.js
│ ├── routes/
│ │ └── auth.js
│ └── utils/
│ └── tokenUtils.js
├── .env
├── app.js
└── server.js
사용자 모델 및 데이터베이스 설정
MongoDB와 Mongoose를 사용하여 사용자 모델을 정의합니다.
// src/models/User.js
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const userSchema = new mongoose.Schema({
email: {
type: String,
required: true,
unique: true,
lowercase: true,
trim: true
},
password: {
type: String,
required: true,
minlength: 6
},
name: {
type: String,
required: true,
trim: true
},
role: {
type: String,
enum: ['user', 'admin'],
default: 'user'
},
refreshTokens: [{
token: String,
createdAt: {
type: Date,
default: Date.now,
expires: 604800 // 7일
}
}]
}, {
timestamps: true
});
// 비밀번호 해싱 미들웨어
userSchema.pre('save', async function(next) {
if (!this.isModified('password')) return next();
try {
const salt = await bcrypt.genSalt(12);
this.password = await bcrypt.hash(this.password, salt);
next();
} catch (error) {
next(error);
}
});
// 비밀번호 검증 메서드
userSchema.methods.comparePassword = async function(candidatePassword) {
return bcrypt.compare(candidatePassword, this.password);
};
module.exports = mongoose.model('User', userSchema);
JWT 토큰 유틸리티 함수
토큰 생성과 검증을 위한 유틸리티 함수들을 작성합니다.
// src/utils/tokenUtils.js
const jwt = require('jsonwebtoken');
class TokenUtils {
static generateAccessToken(payload) {
return jwt.sign(payload, process.env.JWT_ACCESS_SECRET, {
expiresIn: process.env.JWT_ACCESS_EXPIRES_IN || '15m',
issuer: 'your-app-name',
audience: 'your-app-users'
});
}
static generateRefreshToken(payload) {
return jwt.sign(payload, process.env.JWT_REFRESH_SECRET, {
expiresIn: process.env.JWT_REFRESH_EXPIRES_IN || '7d',
issuer: 'your-app-name',
audience: 'your-app-users'
});
}
static verifyAccessToken(token) {
try {
return jwt.verify(token, process.env.JWT_ACCESS_SECRET);
} catch (error) {
throw new Error('Invalid access token');
}
}
static verifyRefreshToken(token) {
try {
return jwt.verify(token, process.env.JWT_REFRESH_SECRET);
} catch (error) {
throw new Error('Invalid refresh token');
}
}
static extractTokenFromHeader(authHeader) {
if (!authHeader || !authHeader.startsWith('Bearer ')) {
throw new Error('Authorization header missing or invalid format');
}
return authHeader.substring(7);
}
}
module.exports = TokenUtils;
인증 컨트롤러 구현
사용자 등록, 로그인, 토큰 갱신 등의 핵심 기능을 구현합니다.
// src/controllers/authController.js
const User = require('../models/User');
const TokenUtils = require('../utils/tokenUtils');
class AuthController {
static async register(req, res) {
try {
const { email, password, name } = req.body;
// 사용자 존재 여부 확인
const existingUser = await User.findOne({ email });
if (existingUser) {
return res.status(409).json({
success: false,
message: '이미 등록된 이메일입니다.'
});
}
// 새 사용자 생성
const user = new User({ email, password, name });
await user.save();
// 토큰 생성
const payload = { userId: user._id, email: user.email, role: user.role };
const accessToken = TokenUtils.generateAccessToken(payload);
const refreshToken = TokenUtils.generateRefreshToken(payload);
// Refresh Token을 데이터베이스에 저장
user.refreshTokens.push({ token: refreshToken });
await user.save();
res.status(201).json({
success: true,
message: '회원가입이 완료되었습니다.',
data: {
user: {
id: user._id,
email: user.email,
name: user.name,
role: user.role
},
accessToken,
refreshToken
}
});
} catch (error) {
res.status(500).json({
success: false,
message: '서버 오류가 발생했습니다.',
error: error.message
});
}
}
static async login(req, res) {
try {
const { email, password } = req.body;
// 사용자 찾기
const user = await User.findOne({ email });
if (!user) {
return res.status(401).json({
success: false,
message: '이메일 또는 비밀번호가 올바르지 않습니다.'
});
}
// 비밀번호 검증
const isPasswordValid = await user.comparePassword(password);
if (!isPasswordValid) {
return res.status(401).json({
success: false,
message: '이메일 또는 비밀번호가 올바르지 않습니다.'
});
}
// 토큰 생성
const payload = { userId: user._id, email: user.email, role: user.role };
const accessToken = TokenUtils.generateAccessToken(payload);
const refreshToken = TokenUtils.generateRefreshToken(payload);
// Refresh Token 저장
user.refreshTokens.push({ token: refreshToken });
await user.save();
res.json({
success: true,
message: '로그인이 완료되었습니다.',
data: {
user: {
id: user._id,
email: user.email,
name: user.name,
role: user.role
},
accessToken,
refreshToken
}
});
} catch (error) {
res.status(500).json({
success: false,
message: '서버 오류가 발생했습니다.',
error: error.message
});
}
}
static async refreshToken(req, res) {
try {
const { refreshToken } = req.body;
if (!refreshToken) {
return res.status(401).json({
success: false,
message: 'Refresh token이 필요합니다.'
});
}
// Refresh Token 검증
const decoded = TokenUtils.verifyRefreshToken(refreshToken);
// 사용자 찾기 및 토큰 유효성 확인
const user = await User.findById(decoded.userId);
if (!user || !user.refreshTokens.some(tokenObj => tokenObj.token === refreshToken)) {
return res.status(401).json({
success: false,
message: '유효하지 않은 refresh token입니다.'
});
}
// 새로운 토큰 생성
const payload = { userId: user._id, email: user.email, role: user.role };
const newAccessToken = TokenUtils.generateAccessToken(payload);
const newRefreshToken = TokenUtils.generateRefreshToken(payload);
// 기존 Refresh Token 제거 및 새 토큰 추가
user.refreshTokens = user.refreshTokens.filter(tokenObj => tokenObj.token !== refreshToken);
user.refreshTokens.push({ token: newRefreshToken });
await user.save();
res.json({
success: true,
data: {
accessToken: newAccessToken,
refreshToken: newRefreshToken
}
});
} catch (error) {
res.status(401).json({
success: false,
message: '토큰 갱신에 실패했습니다.',
error: error.message
});
}
}
static async logout(req, res) {
try {
const { refreshToken } = req.body;
const user = await User.findById(req.user.userId);
if (user && refreshToken) {
user.refreshTokens = user.refreshTokens.filter(tokenObj => tokenObj.token !== refreshToken);
await user.save();
}
res.json({
success: true,
message: '로그아웃이 완료되었습니다.'
});
} catch (error) {
res.status(500).json({
success: false,
message: '로그아웃 처리 중 오류가 발생했습니다.'
});
}
}
}
module.exports = AuthController;인증 미들웨어 구현
API 엔드포인트를 보호하기 위한 미들웨어를 구현합니다.
// src/middleware/authMiddleware.js
const TokenUtils = require('../utils/tokenUtils');
const User = require('../models/User');
const authenticateToken = async (req, res, next) => {
try {
const authHeader = req.headers.authorization;
const token = TokenUtils.extractTokenFromHeader(authHeader);
const decoded = TokenUtils.verifyAccessToken(token);
// 사용자 존재 여부 확인
const user = await User.findById(decoded.userId).select('-password -refreshTokens');
if (!user) {
return res.status(401).json({
success: false,
message: '유효하지 않은 토큰입니다.'
});
}
req.user = decoded;
req.userDocument = user;
next();
} catch (error) {
return res.status(401).json({
success: false,
message: '인증이 필요합니다.',
error: error.message
});
}
};
const requireRole = (roles) => {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({
success: false,
message: '인증이 필요합니다.'
});
}
if (!roles.includes(req.user.role)) {
return res.status(403).json({
success: false,
message: '접근 권한이 없습니다.'
});
}
next();
};
};
module.exports = {
authenticateToken,
requireRole
};
라우터 설정 및 API 엔드포인트
인증 관련 라우터를 설정합니다.
// src/routes/auth.js
const express = require('express');
const AuthController = require('../controllers/authController');
const { authenticateToken } = require('../middleware/authMiddleware');
const router = express.Router();
router.post('/register', AuthController.register);
router.post('/login', AuthController.login);
router.post('/refresh', AuthController.refreshToken);
router.post('/logout', authenticateToken, AuthController.logout);
// 보호된 라우트 예시
router.get('/profile', authenticateToken, (req, res) => {
res.json({
success: true,
data: {
user: req.userDocument
}
});
});
module.exports = router;
애플리케이션 메인 설정
Express 애플리케이션의 메인 설정을 구성합니다.
// app.js
const express = require('express');
const mongoose = require('mongoose');
const cors = require('cors');
const helmet = require('helmet');
require('dotenv').config();
const authRoutes = require('./src/routes/auth');
const app = express();
// 보안 및 미들웨어 설정
app.use(helmet());
app.use(cors({
origin: process.env.CLIENT_URL || 'http://localhost:3000',
credentials: true
}));
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true }));
// 데이터베이스 연결
mongoose.connect(process.env.MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true
});
// 라우터 설정
app.use('/api/auth', authRoutes);
// 에러 핸들링 미들웨어
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({
success: false,
message: '서버 내부 오류가 발생했습니다.'
});
});
// 404 핸들러
app.use('*', (req, res) => {
res.status(404).json({
success: false,
message: '요청한 리소스를 찾을 수 없습니다.'
});
});
module.exports = app;
JWT 인증 시스템 아키텍처
전체 시스템의 아키텍처는 다음과 같습니다:
클라이언트 요청 → Express 서버 → 인증 미들웨어 → 컨트롤러 → MongoDB
인증 플로우:
1. 사용자 로그인 → JWT 토큰 생성 (Access + Refresh)
2. API 요청 시 Access Token을 Header에 포함
3. 미들웨어에서 토큰 검증
4. 토큰 만료 시 Refresh Token으로 갱신
5. 로그아웃 시 Refresh Token 무효화
보안 고려사항 및 베스트 프랙티스
JWT 인증 시스템을 구현할 때 다음 사항들을 반드시 고려해야 합니다:
- 토큰 만료 시간: Access Token은 짧게(15분), Refresh Token은 길게(7일) 설정
- 시크릿 키 관리: 환경 변수로 관리하고 충분히 복잡하게 생성
- HTTPS 사용: 프로덕션 환경에서는 반드시 HTTPS 사용
- 토큰 저장: 클라이언트에서는 httpOnly 쿠키 사용 권장
- 브루트포스 방어: 로그인 시도 횟수 제한 구현
- 토큰 블랙리스트: 필요시 토큰 무효화 메커니즘 구현
마무리
JWT 인증 시스템은 현대 웹 애플리케이션에서 널리 사용되는 표준적인 인증 방식입니다. 이 가이드에서 제시한 구현 방법을 통해 확장 가능하고 안전한 인증 시스템을 구축할 수 있습니다. 실제 프로덕션 환경에서는 추가적인 보안 조치와 모니터링, 로깅 시스템을 함께 구현하여 더욱 견고한 시스템을 만들어야 합니다.
관련 게시글
데이터베이스 Indexing 최적화 전략: Node.js API 성능 향상 가이드
Node.js API 백엔드 서버의 성능을 극대화하기 위한 데이터베이스 Indexing 최적화 전략을 심층적으로 다룹니다. B-tree, 복합 인덱스, Covering Index 등 다양한 기법과 실제 활용 예시를 통해 쿼리 속도를 향상시키는 방법을 알아보세요.
JWT Authentication System 구현 가이드: Node.js API 서버 구축
Node.js 백엔드 API 서버에서 JWT(JSON Web Token)를 활용한 안전하고 확장 가능한 인증 시스템을 구축하는 방법을 심층적으로 다룹니다. 아키텍처 설계부터 실제 코드 구현까지 자세히 설명합니다.
gRPC vs REST: Modern API Architecture Deep Dive
백엔드 API 아키텍처의 핵심인 gRPC와 REST를 비교 분석합니다. 성능, 개발 편의성, 사용 사례를 통해 각 기술의 장단점을 깊이 있게 탐구하고, Node.js 기반의 구현 예시를 제공하여 최적의 API 선택 가이드를 제시합니다.