Node.js API 개발을 위한 JWT Authentication 시스템 구현 가이드
Node.js 환경에서 JWT(JSON Web Token)를 활용한 API 인증 시스템 구현 가이드를 제공합니다. Access Token과 Refresh Token 기반의 아키텍처, Express.js를 이용한 실제 코드 구현, 그리고 JWT 보안 고려사항을 상세히 다룹니다.
Node.js API 개발을 위한 JWT Authentication 시스템 구현 가이드
현대 웹 애플리케이션에서 API 보안은 필수적인 요소입니다. 사용자 인증 및 권한 부여는 API를 안전하게 보호하고 신뢰할 수 있는 서비스를 제공하기 위한 핵심 기능인데요. 이 글에서는 Node.js 환경에서 JWT(JSON Web Token)를 활용하여 강력하고 효율적인 인증 시스템을 구축하는 방법을 심층적으로 다룹니다. JWT 기반 인증의 원리부터 실제 구현 코드, 그리고 보안 고려사항까지 함께 살펴보며, 여러분의 백엔드 API 개발 역량을 한층 더 강화하는 데 도움을 드리고자 합니다.
JWT (JSON Web Token)란 무엇인가요?
JWT는 클라이언트와 서버 간 정보를 안전하게 주고받기 위해 사용되는 간결하고 자체 포함적인(self-contained) 토큰입니다. 주로 인증(Authentication)과 권한 부여(Authorization)에 사용되며, 서버가 클라이언트의 상태를 유지할 필요가 없는(Stateless) 특징을 가집니다.
JWT는 크게 세 부분으로 나뉘며, 각 부분은 점(.)으로 구분됩니다.
- Header (헤더)
토큰의 타입(JWT)과 서명에 사용된 암호화 알고리즘(예: HS256, RS256)을 명시합니다. 이 정보는 Base64Url로 인코딩됩니다.
{
"alg": "HS256",
"typ": "JWT"
}
- Payload (페이로드)
실제 전달하려는 정보인 '클레임(Claim)'을 담는 부분입니다. 클레임은 사용자 ID, 역할, 토큰 만료 시간 등과 같은 정보를 포함할 수 있습니다. 페이로드 역시 Base64Url로 인코딩됩니다. 클레임은 다음 세 가지 유형으로 나뉩니다.
- 등록된 클레임 (Registered Claims):
iss(발행자),exp(만료 시간),sub(주제) 등 미리 정의된 클레임입니다. 필수는 아니지만, 상호운용성을 높이기 위해 권장됩니다. - 공개 클레임 (Public Claims): JWT를 사용하는 사람들이 충돌을 방지하기 위해 정의할 수 있는 클레임입니다.
- 비공개 클레임 (Private Claims): 클라이언트와 서버 간에 정보를 공유하기 위해 생성되는 사용자 정의 클레임입니다.
{
"sub": "1234567890",
"name": "John Doe",
"admin": true,
"exp": 1678886400 // 토큰 만료 시간 (Unix Time)
}
- Signature (서명)
인코딩된 헤더, 인코딩된 페이로드, 그리고 서버의 비밀 키(Secret Key)를 사용하여 생성됩니다. 이 서명은 토큰의 무결성을 검증하는 데 사용됩니다. 즉, 토큰이 전송 중에 변조되었는지 여부를 확인할 수 있게 해줍니다.
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
JWT는 이 세 부분이 .으로 연결된 문자열 형태로 클라이언트에 전달됩니다. 예를 들어, eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c와 같은 형태입니다.
JWT 기반 인증 시스템 아키텍처
JWT 기반 인증 시스템은 일반적으로 Access Token과 Refresh Token이라는 두 가지 유형의 토큰을 활용하여 보안과 사용자 편의성을 동시에 확보합니다.
- Access Token: 실제 API 리소스에 접근할 때 사용되는 토큰입니다. 짧은 만료 시간을 가지며, 탈취되더라도 피해를 최소화할 수 있도록 설계됩니다.
- Refresh Token: Access Token이 만료되었을 때 새로운 Access Token을 발급받기 위해 사용되는 토큰입니다. Access Token보다 긴 만료 시간을 가지며, 한 번만 사용되거나(One-Time Use) 서버 측 DB에 저장되어 관리됩니다.
다음은 일반적인 JWT 인증 흐름을 보여주는 아키텍처 다이어그램입니다.
+-------------------+ +-------------------+ +-------------------+
| Client | | Node.js API | | Database |
| (Web Browser/App) | | Server | | (User, RefreshToken)|
+-------------------+ +-------------------+ +-------------------+
| | |
| 1. POST /login (ID, PW) | |
|---------------------------------->| |
| | 2. 사용자 인증 및 토큰 생성 |
| |<--------------------------|
| | (Access Token, Refresh Token) |
|<----------------------------------| |
| 3. 토큰 저장 (Access: 메모리, Refresh: 쿠키/DB) | |
| | |
| 4. GET /protected-resource | |
| (Authorization: Bearer <Access Token>) | |
|---------------------------------->| |
| | 5. Access Token 검증 |
| |-------------------------->|
| | 6. 리소스 응답 |
|<----------------------------------| |
| | |
| (Access Token 만료 시) | |
| 7. POST /refresh-token | |
| (Refresh Token) | |
|---------------------------------->| |
| | 8. Refresh Token 검증 (DB 확인) |
| |<--------------------------|
| | 9. 새 Access Token, Refresh Token 생성 |
|<----------------------------------| |
| 10. 새 토큰 저장 | |
| | |
흐름 설명:
- 사용자가 클라이언트에서 아이디와 비밀번호로 로그인 요청을 보냅니다.
- Node.js API 서버는 전달받은 정보를 데이터베이스와 비교하여 사용자를 인증합니다. 인증에 성공하면 Access Token과 Refresh Token을 생성합니다.
- 생성된 두 토큰을 클라이언트에 응답으로 보냅니다. 클라이언트는 Access Token을 메모리(세션 스토리지)에 저장하고, Refresh Token은 더 안전하게 HttpOnly 쿠키 또는 로컬 스토리지에 저장합니다.
- 클라이언트는 보호된 API 리소스에 접근할 때마다 Access Token을 HTTP
Authorization헤더에Bearer스키마와 함께 담아 전송합니다. - 서버는 요청 시 전달된 Access Token의 유효성을 검증합니다. 서명 확인, 만료 시간 확인 등을 통해 토큰이 유효한지 확인합니다.
- 토큰이 유효하면 요청을 처리하고 클라이언트에 응답을 보냅니다.
- Access Token이 만료되면, 클라이언트는 Refresh Token을 사용하여 새로운 Access Token을 요청합니다.
- 서버는 Refresh Token의 유효성을 검증하고, 데이터베이스에 저장된 Refresh Token과 일치하는지 확인합니다.
- 유효한 Refresh Token이라면, 서버는 새로운 Access Token과 Refresh Token을 생성하여 클라이언트에 다시 보냅니다.
- 클라이언트는 새롭게 발급받은 토큰들을 저장하고 다음 API 요청에 사용합니다.
Node.js 환경 설정 및 기본 API 구성
Node.js 환경에서 Express.js 프레임워크를 사용하여 JWT 인증 시스템을 구축해 보겠습니다. 먼저 프로젝트를 초기화하고 필요한 패키지를 설치합니다.
# 프로젝트 디렉토리 생성 및 이동
mkdir jwt-auth-server
cd jwt-auth-server
# npm 프로젝트 초기화
npm init -y
# 필요한 패키지 설치
# express: 웹 서버 프레임워크
# jsonwebtoken: JWT 생성 및 검증
# bcrypt: 비밀번호 해싱
# dotenv: 환경 변수 관리
# mongoose: MongoDB ORM (데이터베이스 예시)
npm install express jsonwebtoken bcrypt dotenv mongoose
프로젝트 루트에 .env 파일을 생성하여 환경 변수를 관리합니다. 이 파일은 .gitignore에 추가하여 버전 관리에서 제외해야 합니다.
# .env
PORT=3000
MONGO_URI=mongodb://localhost:27017/jwt_auth_db
ACCESS_TOKEN_SECRET=your_access_token_secret_key
REFRESH_TOKEN_SECRET=your_refresh_token_secret_key
이제 app.js 파일을 생성하고 기본적인 Express 서버와 MongoDB 연결을 설정합니다.
// app.js
require('dotenv').config(); // .env 파일 로드
const express = require('express');
const mongoose = require('mongoose');
const app = express();
const PORT = process.env.PORT || 3000;
// JSON 요청 본문을 파싱하기 위한 미들웨어
app.use(express.json());
// MongoDB 연결
mongoose.connect(process.env.MONGO_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
})
.then(() => console.log('MongoDB connected successfully'))
.catch(err => console.error('MongoDB connection error:', err));
// 기본 라우트
app.get('/', (req, res) => {
res.send('JWT Authentication Server is running!');
});
// 서버 시작
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
JWT 생성 및 로그인 API 구현
사용자 인증을 위해 간단한 사용자 모델과 로그인 API를 구현해 보겠습니다.
먼저 models/User.js 파일을 생성하여 사용자 스키마와 비밀번호 해싱 로직을 정의합니다.
// models/User.js
const mongoose = require('mongoose');
const bcrypt = require('bcrypt');
const UserSchema = new mongoose.Schema({
username: { type: String, required: true, unique: true },
password: { type: String, required: true },
});
// 비밀번호 저장 전에 해싱
UserSchema.pre('save', async function(next) {
if (!this.isModified('password')) return next();
const salt = await bcrypt.genSalt(10);
this.password = await bcrypt.hash(this.password, salt);
next();
});
// 비밀번호 비교 메서드
UserSchema.methods.comparePassword = async function(candidatePassword) {
return bcrypt.compare(candidatePassword, this.password);
};
module.exports = mongoose.model('User', UserSchema);
다음으로 Refresh Token을 저장할 models/RefreshToken.js 모델을 생성합니다.
// models/RefreshToken.js
const mongoose = require('mongoose');
const RefreshTokenSchema = new mongoose.Schema({
token: { type: String, required: true },
userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
createdAt: { type: Date, default: Date.now, expires: '7d' } // 7일 후 자동 삭제
});
module.exports = mongoose.model('RefreshToken', RefreshTokenSchema);
이제 controllers/authController.js 파일을 생성하여 로그인 로직과 JWT 생성 로직을 구현합니다.
// controllers/authController.js
const jwt = require('jsonwebtoken');
const User = require('../models/User');
const RefreshToken = require('../models/RefreshToken');
// Access Token과 Refresh Token을 생성하는 헬퍼 함수
const generateTokens = (user) => {
// Access Token은 짧은 유효 기간 (예: 15분)
const accessToken = jwt.sign({ id: user._id, username: user.username }, process.env.ACCESS_TOKEN_SECRET, { expiresIn: '15m' });
// Refresh Token은 긴 유효 기간 (예: 7일)
const refreshToken = jwt.sign({ id: user._id, username: user.username }, process.env.REFRESH_TOKEN_SECRET, { expiresIn: '7d' });
return { accessToken, refreshToken };
};
// 로그인 API
exports.login = async (req, res) => {
const { username, password } = req.body;
try {
// 1. 사용자 존재 여부 확인
const user = await User.findOne({ username });
if (!user) {
return res.status(400).json({ message: 'Invalid credentials' });
}
// 2. 비밀번호 일치 여부 확인
const isMatch = await user.comparePassword(password);
if (!isMatch) {
return res.status(400).json({ message: 'Invalid credentials' });
}
// 3. Access Token 및 Refresh Token 생성
const { accessToken, refreshToken } = generateTokens(user);
// 4. Refresh Token을 DB에 저장
// 이전 Refresh Token이 있다면 삭제하고 새로 저장하는 로직도 고려할 수 있습니다.
await RefreshToken.deleteOne({ userId: user._id }); // 기존 토큰 삭제 (선택 사항)
const newRefreshToken = new RefreshToken({ token: refreshToken, userId: user._id });
await newRefreshToken.save();
// 5. 클라이언트에 토큰 응답
res.json({ accessToken, refreshToken });
} catch (error) {
console.error('Login error:', error);
res.status(500).json({ message: 'Server error during login' });
}
};
// (추후 Refresh Token 갱신 로직 추가 예정)
app.js에 로그인 라우트를 추가합니다.
// app.js (부분)
// ...
const authController = require('./controllers/authController');
// 인증 라우트
app.post('/api/login', authController.login);
// ...
테스트를 위해 간단한 사용자 등록 API를 추가할 수도 있습니다.
// controllers/authController.js (추가)
exports.register = async (req, res) => {
const { username, password } = req.body;
try {
const existingUser = await User.findOne({ username });
if (existingUser) {
return res.status(409).json({ message: 'Username already exists' });
}
const newUser = new User({ username, password });
await newUser.save();관련 게시글
GraphQL API 설계 패턴 가이드: Best Practices for Scalable API Design
GraphQL API를 효과적으로 설계하기 위한 핵심 패턴과 모범 사례를 Node.js 환경에서 Backend 개발 관점에서 심도 있게 다룹니다. 스키마 디자인, 데이터 페칭 최적화, 보안 및 아키텍처 전략을 통해 확장 가능하고 유지보수하기 쉬운 API를 구축하는 방법을 안내합니다.
JWT Authentication System 구현 가이드: Node.js 백엔드 개발 중심
Node.js 환경에서 JWT(JSON Web Token) 기반의 안전하고 효율적인 인증 시스템을 구현하는 방법을 상세히 안내합니다. API 서버 개발에 필요한 아키텍처, 토큰 관리 전략, 코드 예시를 다룹니다.
Database Indexing Optimization: Strategies for Backend Performance
백엔드 서버 성능의 핵심인 데이터베이스 인덱싱 최적화 전략을 Node.js API 개발 관점에서 심층 분석합니다. 쿼리 성능 향상을 위한 실용적인 팁을 제공합니다.