Node.js 인증과 보안 | JWT, bcrypt, 세션, OAuth
이 글의 핵심
Node.js 인증과 보안에 대한 실전 가이드입니다. JWT, bcrypt, 세션, OAuth 등을 예제와 함께 설명합니다.
들어가며
인증 vs 인가
HTTP는 상태가 없어서(stateless) 매 요청마다 “누구인지”를 다시 증명해야 합니다. 그때 세션 쿠키나 JWT로 “이미 로그인했다”는 정보를 실어 보내고, 비밀번호는 bcrypt 등으로 해시만 저장합니다. 토큰은 출입증에 가깝고, 권한(관리자만 삭제 등)은 그 토큰 안의 역할·스코프로 인가 단계에서 나눕니다.
인증 (Authentication):
- “당신은 누구인가?” (신원 확인)
- 로그인, 회원가입
인가 (Authorization):
- “당신은 무엇을 할 수 있는가?” (권한 확인)
- 역할 기반 접근 제어 (RBAC)
인증 방식:
- ✅ JWT (JSON Web Token): Stateless, 확장성 좋음
- ✅ 세션 (Session): Stateful, 서버에서 제어
- ✅ OAuth 2.0: 소셜 로그인 (Google, GitHub)
- ✅ API Key: 간단한 API 인증
1. 비밀번호 해싱 (bcrypt)
설치
npm install bcrypt
기본 사용법
const bcrypt = require('bcrypt');
// 비밀번호 해싱
async function hashPassword(password) {
const saltRounds = 10; // 해싱 강도 (10~12 권장)
const hash = await bcrypt.hash(password, saltRounds);
return hash;
}
// 비밀번호 확인
async function verifyPassword(password, hash) {
const isValid = await bcrypt.compare(password, hash);
return isValid;
}
// 사용
async function main() {
const password = 'mypassword123';
// 해싱
const hash = await hashPassword(password);
console.log('해시:', hash);
// $2b$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy
// 확인
const isValid = await verifyPassword(password, hash);
console.log('비밀번호 일치:', isValid); // true
const isInvalid = await verifyPassword('wrongpassword', hash);
console.log('잘못된 비밀번호:', isInvalid); // false
}
main();
회원가입 구현
const express = require('express');
const bcrypt = require('bcrypt');
const User = require('./models/User');
const app = express();
app.use(express.json());
app.post('/auth/register', async (req, res) => {
try {
const { email, password, name } = req.body;
// 유효성 검사
if (!email || !password || !name) {
return res.status(400).json({ error: '모든 필드가 필요합니다' });
}
if (password.length < 8) {
return res.status(400).json({ error: '비밀번호는 8자 이상이어야 합니다' });
}
// 중복 확인
const existingUser = await User.findOne({ email });
if (existingUser) {
return res.status(400).json({ error: '이미 존재하는 이메일입니다' });
}
// 비밀번호 해싱
const hashedPassword = await bcrypt.hash(password, 10);
// 사용자 생성
const user = await User.create({
email,
password: hashedPassword,
name
});
// 비밀번호 제외하고 응답
const { password: _, ...userWithoutPassword } = user.toObject();
res.status(201).json({
message: '회원가입 성공',
user: userWithoutPassword
});
} catch (err) {
res.status(500).json({ error: err.message });
}
});
2. JWT (JSON Web Token)
설치
npm install jsonwebtoken
JWT 생성 및 검증
const jwt = require('jsonwebtoken');
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
// 토큰 생성
function generateToken(payload) {
return jwt.sign(
payload,
JWT_SECRET,
{ expiresIn: '1h' } // 1시간 후 만료
);
}
// 토큰 검증
function verifyToken(token) {
try {
const decoded = jwt.verify(token, JWT_SECRET);
return decoded;
} catch (err) {
if (err.name === 'TokenExpiredError') {
throw new Error('토큰이 만료되었습니다');
}
throw new Error('유효하지 않은 토큰입니다');
}
}
// 사용
const token = generateToken({ id: 123, email: '[email protected]' });
console.log('토큰:', token);
const decoded = verifyToken(token);
console.log('디코딩:', decoded);
// { id: 123, email: '[email protected]', iat: 1234567890, exp: 1234571490 }
로그인 구현
const express = require('express');
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const User = require('./models/User');
const app = express();
app.use(express.json());
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
app.post('/auth/login', async (req, res) => {
try {
const { email, password } = req.body;
// 사용자 찾기
const user = await User.findOne({ email });
if (!user) {
return res.status(401).json({ error: '이메일 또는 비밀번호가 잘못되었습니다' });
}
// 비밀번호 확인
const isValid = await bcrypt.compare(password, user.password);
if (!isValid) {
return res.status(401).json({ error: '이메일 또는 비밀번호가 잘못되었습니다' });
}
// JWT 토큰 생성
const token = jwt.sign(
{ id: user._id, email: user.email, role: user.role },
JWT_SECRET,
{ expiresIn: '1h' }
);
res.json({
message: '로그인 성공',
token,
user: {
id: user._id,
email: user.email,
name: user.name,
role: user.role
}
});
} catch (err) {
res.status(500).json({ error: err.message });
}
});
인증 미들웨어
// middlewares/auth.js
const jwt = require('jsonwebtoken');
const User = require('../models/User');
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
async function authenticate(req, res, next) {
try {
// 헤더에서 토큰 추출
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: '인증 토큰이 필요합니다' });
}
const token = authHeader.substring(7);
// 토큰 검증
const decoded = jwt.verify(token, JWT_SECRET);
// 사용자 조회
const user = await User.findById(decoded.id).select('-password');
if (!user) {
return res.status(401).json({ error: '사용자를 찾을 수 없습니다' });
}
// req에 사용자 정보 추가
req.user = user;
next();
} catch (err) {
if (err.name === 'TokenExpiredError') {
return res.status(401).json({ error: '토큰이 만료되었습니다' });
}
res.status(401).json({ error: '유효하지 않은 토큰입니다' });
}
}
module.exports = { authenticate };
// 사용
const { authenticate } = require('./middlewares/auth');
app.get('/api/profile', authenticate, (req, res) => {
res.json({ user: req.user });
});
권한 확인 미들웨어
// middlewares/authorize.js
function authorize(...roles) {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({ error: '인증이 필요합니다' });
}
if (!roles.includes(req.user.role)) {
return res.status(403).json({ error: '권한이 없습니다' });
}
next();
};
}
module.exports = { authorize };
// 사용
const { authenticate } = require('./middlewares/auth');
const { authorize } = require('./middlewares/authorize');
// 관리자만 접근 가능
app.delete('/api/users/:id',
authenticate,
authorize('admin'),
async (req, res) => {
await User.findByIdAndDelete(req.params.id);
res.status(204).send();
}
);
// 관리자 또는 매니저
app.get('/api/reports',
authenticate,
authorize('admin', 'manager'),
(req, res) => {
res.json({ reports: [] });
}
);
3. Refresh Token
구현
// models/RefreshToken.js
const mongoose = require('mongoose');
const refreshTokenSchema = new mongoose.Schema({
token: {
type: String,
required: true,
unique: true
},
user: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true
},
expiresAt: {
type: Date,
required: true
},
createdAt: {
type: Date,
default: Date.now
}
});
// 만료된 토큰 자동 삭제
refreshTokenSchema.index({ expiresAt: 1 }, { expireAfterSeconds: 0 });
module.exports = mongoose.model('RefreshToken', refreshTokenSchema);
// auth.js
const jwt = require('jsonwebtoken');
const crypto = require('crypto');
const RefreshToken = require('./models/RefreshToken');
const JWT_SECRET = process.env.JWT_SECRET;
const REFRESH_SECRET = process.env.REFRESH_SECRET;
// Access Token 생성 (짧은 유효기간)
function generateAccessToken(user) {
return jwt.sign(
{ id: user._id, email: user.email, role: user.role },
JWT_SECRET,
{ expiresIn: '15m' } // 15분
);
}
// Refresh Token 생성 (긴 유효기간)
async function generateRefreshToken(user) {
const token = crypto.randomBytes(40).toString('hex');
await RefreshToken.create({
token,
user: user._id,
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) // 7일
});
return token;
}
// 로그인
app.post('/auth/login', async (req, res) => {
try {
const { email, password } = req.body;
const user = await User.findOne({ email });
if (!user || !(await bcrypt.compare(password, user.password))) {
return res.status(401).json({ error: '이메일 또는 비밀번호가 잘못되었습니다' });
}
const accessToken = generateAccessToken(user);
const refreshToken = await generateRefreshToken(user);
res.json({
accessToken,
refreshToken,
expiresIn: 900 // 15분 (초 단위)
});
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// 토큰 갱신
app.post('/auth/refresh', async (req, res) => {
try {
const { refreshToken } = req.body;
if (!refreshToken) {
return res.status(400).json({ error: 'Refresh Token이 필요합니다' });
}
// Refresh Token 확인
const tokenDoc = await RefreshToken.findOne({ token: refreshToken })
.populate('user');
if (!tokenDoc) {
return res.status(401).json({ error: '유효하지 않은 Refresh Token' });
}
if (tokenDoc.expiresAt < new Date()) {
await RefreshToken.deleteOne({ token: refreshToken });
return res.status(401).json({ error: 'Refresh Token이 만료되었습니다' });
}
// 새 Access Token 발급
const accessToken = generateAccessToken(tokenDoc.user);
res.json({
accessToken,
expiresIn: 900
});
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// 로그아웃
app.post('/auth/logout', async (req, res) => {
try {
const { refreshToken } = req.body;
if (refreshToken) {
await RefreshToken.deleteOne({ token: refreshToken });
}
res.json({ message: '로그아웃 성공' });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
4. 세션 (Session)
설치
npm install express-session connect-mongo
기본 설정
const express = require('express');
const session = require('express-session');
const MongoStore = require('connect-mongo');
const app = express();
app.use(session({
secret: process.env.SESSION_SECRET || 'your-secret-key',
resave: false,
saveUninitialized: false,
store: MongoStore.create({
mongoUrl: 'mongodb://localhost:27017/mydb',
ttl: 24 * 60 * 60 // 1일 (초 단위)
}),
cookie: {
maxAge: 24 * 60 * 60 * 1000, // 1일 (밀리초)
httpOnly: true,
secure: process.env.NODE_ENV === 'production', // HTTPS에서만
sameSite: 'strict'
}
}));
세션 사용
// 로그인
app.post('/auth/login', async (req, res) => {
try {
const { email, password } = req.body;
const user = await User.findOne({ email });
if (!user || !(await bcrypt.compare(password, user.password))) {
return res.status(401).json({ error: '이메일 또는 비밀번호가 잘못되었습니다' });
}
// 세션에 사용자 정보 저장
req.session.userId = user._id;
req.session.email = user.email;
req.session.role = user.role;
res.json({
message: '로그인 성공',
user: {
id: user._id,
email: user.email,
name: user.name
}
});
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// 로그아웃
app.post('/auth/logout', (req, res) => {
req.session.destroy((err) => {
if (err) {
return res.status(500).json({ error: '로그아웃 실패' });
}
res.clearCookie('connect.sid');
res.json({ message: '로그아웃 성공' });
});
});
// 세션 확인 미들웨어
function requireAuth(req, res, next) {
if (!req.session.userId) {
return res.status(401).json({ error: '로그인이 필요합니다' });
}
next();
}
// 보호된 라우트
app.get('/api/profile', requireAuth, async (req, res) => {
try {
const user = await User.findById(req.session.userId).select('-password');
res.json({ user });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
5. OAuth 2.0 (소셜 로그인)
Passport.js 설치
npm install passport passport-google-oauth20 passport-github2
Google OAuth
// config/passport.js
const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;
const User = require('../models/User');
passport.use(new GoogleStrategy({
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: '/auth/google/callback'
}, async (accessToken, refreshToken, profile, done) => {
try {
// 기존 사용자 찾기
let user = await User.findOne({ googleId: profile.id });
if (!user) {
// 새 사용자 생성
user = await User.create({
googleId: profile.id,
email: profile.emails[0].value,
name: profile.displayName,
avatar: profile.photos[0].value
});
}
done(null, user);
} catch (err) {
done(err, null);
}
}));
passport.serializeUser((user, done) => {
done(null, user._id);
});
passport.deserializeUser(async (id, done) => {
try {
const user = await User.findById(id);
done(null, user);
} catch (err) {
done(err, null);
}
});
module.exports = passport;
// app.js
const express = require('express');
const session = require('express-session');
const passport = require('./config/passport');
const app = express();
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false
}));
app.use(passport.initialize());
app.use(passport.session());
// Google 로그인 시작
app.get('/auth/google',
passport.authenticate('google', { scope: ['profile', 'email'] })
);
// Google 콜백
app.get('/auth/google/callback',
passport.authenticate('google', { failureRedirect: '/login' }),
(req, res) => {
res.redirect('/dashboard');
}
);
// 로그아웃
app.get('/auth/logout', (req, res) => {
req.logout((err) => {
if (err) {
return res.status(500).json({ error: '로그아웃 실패' });
}
res.redirect('/');
});
});
// 인증 확인
function ensureAuthenticated(req, res, next) {
if (req.isAuthenticated()) {
return next();
}
res.redirect('/login');
}
app.get('/dashboard', ensureAuthenticated, (req, res) => {
res.json({ user: req.user });
});
6. 보안 베스트 프랙티스
Helmet (보안 헤더)
npm install helmet
const helmet = require('helmet');
app.use(helmet());
// 커스텀 설정
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
scriptSrc: ["'self'"]
}
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true
}
}));
Rate Limiting
npm install express-rate-limit
const rateLimit = require('express-rate-limit');
// 일반 요청 제한
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15분
max: 100, // 최대 100개 요청
message: '너무 많은 요청이 발생했습니다. 나중에 다시 시도하세요.'
});
app.use('/api/', limiter);
// 로그인 요청 제한 (더 엄격)
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5, // 15분에 5번만
skipSuccessfulRequests: true // 성공한 요청은 카운트 안 함
});
app.post('/auth/login', loginLimiter, async (req, res) => {
// ...
});
CORS 설정
npm install cors
const cors = require('cors');
// 모든 도메인 허용 (개발 환경)
app.use(cors());
// 특정 도메인만 허용 (프로덕션)
app.use(cors({
origin: 'https://yourdomain.com',
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization']
}));
// 동적 origin
app.use(cors({
origin: (origin, callback) => {
const allowedOrigins = ['https://yourdomain.com', 'https://admin.yourdomain.com'];
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('CORS 정책 위반'));
}
},
credentials: true
}));
입력 검증
npm install express-validator
const { body, validationResult } = require('express-validator');
app.post('/auth/register',
// 검증 규칙
body('email')
.isEmail().withMessage('유효한 이메일이 아닙니다')
.normalizeEmail(),
body('password')
.isLength({ min: 8 }).withMessage('비밀번호는 8자 이상이어야 합니다')
.matches(/[A-Z]/).withMessage('대문자가 포함되어야 합니다')
.matches(/[a-z]/).withMessage('소문자가 포함되어야 합니다')
.matches(/[0-9]/).withMessage('숫자가 포함되어야 합니다')
.matches(/[@$!%*?&#]/).withMessage('특수문자가 포함되어야 합니다'),
body('name')
.trim()
.notEmpty().withMessage('이름이 필요합니다')
.isLength({ min: 2, max: 50 }).withMessage('이름은 2-50자여야 합니다'),
// 핸들러
async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
// 회원가입 로직
// ...
}
);
SQL Injection 방지
// ❌ SQL Injection 취약
async function vulnerable(email) {
const query = `SELECT * FROM users WHERE email = '${email}'`;
const [rows] = await pool.query(query);
return rows;
}
// 공격: email = "' OR '1'='1"
// ✅ Prepared Statement 사용
async function safe(email) {
const [rows] = await pool.query(
'SELECT * FROM users WHERE email = ?',
[email]
);
return rows;
}
// ✅ ORM 사용
async function safest(email) {
return await User.findOne({ email });
}
XSS 방지
npm install xss
const xss = require('xss');
app.post('/api/posts', async (req, res) => {
const { title, content } = req.body;
// XSS 필터링
const sanitizedTitle = xss(title);
const sanitizedContent = xss(content);
const post = await Post.create({
title: sanitizedTitle,
content: sanitizedContent
});
res.status(201).json(post);
});
7. 실전 프로젝트: 완전한 인증 시스템
// models/User.js
const mongoose = require('mongoose');
const bcrypt = require('bcrypt');
const userSchema = new mongoose.Schema({
email: {
type: String,
required: true,
unique: true,
lowercase: true
},
password: {
type: String,
required: true,
minlength: 8
},
name: {
type: String,
required: true
},
role: {
type: String,
enum: ['user', 'admin'],
default: 'user'
},
isVerified: {
type: Boolean,
default: false
},
verificationToken: String,
resetPasswordToken: String,
resetPasswordExpires: Date,
lastLogin: Date
}, {
timestamps: true
});
// 비밀번호 해싱 (저장 전)
userSchema.pre('save', async function(next) {
if (!this.isModified('password')) {
return next();
}
this.password = await bcrypt.hash(this.password, 10);
next();
});
// 비밀번호 확인 메서드
userSchema.methods.comparePassword = async function(candidatePassword) {
return await bcrypt.compare(candidatePassword, this.password);
};
module.exports = mongoose.model('User', userSchema);
// routes/auth.js
const express = require('express');
const router = express.Router();
const jwt = require('jsonwebtoken');
const crypto = require('crypto');
const User = require('../models/User');
const { sendEmail } = require('../utils/email');
const JWT_SECRET = process.env.JWT_SECRET;
// 회원가입
router.post('/register', async (req, res) => {
try {
const { email, password, name } = req.body;
// 중복 확인
const existingUser = await User.findOne({ email });
if (existingUser) {
return res.status(400).json({ error: '이미 존재하는 이메일입니다' });
}
// 이메일 인증 토큰 생성
const verificationToken = crypto.randomBytes(32).toString('hex');
// 사용자 생성
const user = await User.create({
email,
password,
name,
verificationToken
});
// 인증 이메일 발송
const verificationUrl = `${req.protocol}://${req.get('host')}/auth/verify/${verificationToken}`;
await sendEmail({
to: email,
subject: '이메일 인증',
text: `다음 링크를 클릭하여 이메일을 인증하세요: ${verificationUrl}`
});
res.status(201).json({
message: '회원가입 성공. 이메일을 확인하세요.',
user: {
id: user._id,
email: user.email,
name: user.name
}
});
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// 이메일 인증
router.get('/verify/:token', async (req, res) => {
try {
const user = await User.findOne({ verificationToken: req.params.token });
if (!user) {
return res.status(400).json({ error: '유효하지 않은 토큰입니다' });
}
user.isVerified = true;
user.verificationToken = undefined;
await user.save();
res.json({ message: '이메일 인증 완료' });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// 비밀번호 재설정 요청
router.post('/forgot-password', async (req, res) => {
try {
const { email } = req.body;
const user = await User.findOne({ email });
if (!user) {
// 보안상 사용자 존재 여부를 알려주지 않음
return res.json({ message: '이메일을 확인하세요' });
}
// 재설정 토큰 생성
const resetToken = crypto.randomBytes(32).toString('hex');
user.resetPasswordToken = resetToken;
user.resetPasswordExpires = Date.now() + 3600000; // 1시간
await user.save();
// 이메일 발송
const resetUrl = `${req.protocol}://${req.get('host')}/auth/reset-password/${resetToken}`;
await sendEmail({
to: email,
subject: '비밀번호 재설정',
text: `다음 링크를 클릭하여 비밀번호를 재설정하세요: ${resetUrl}`
});
res.json({ message: '이메일을 확인하세요' });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// 비밀번호 재설정
router.post('/reset-password/:token', async (req, res) => {
try {
const { password } = req.body;
const user = await User.findOne({
resetPasswordToken: req.params.token,
resetPasswordExpires: { $gt: Date.now() }
});
if (!user) {
return res.status(400).json({ error: '유효하지 않거나 만료된 토큰입니다' });
}
user.password = password;
user.resetPasswordToken = undefined;
user.resetPasswordExpires = undefined;
await user.save();
res.json({ message: '비밀번호가 재설정되었습니다' });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
module.exports = router;
8. 자주 발생하는 문제
문제 1: JWT 토큰 무효화
문제: JWT는 서버에서 무효화할 수 없음
해결:
// 블랙리스트 방식
const tokenBlacklist = new Set();
app.post('/auth/logout', authenticate, (req, res) => {
const token = req.headers.authorization.substring(7);
tokenBlacklist.add(token);
res.json({ message: '로그아웃 성공' });
});
// 미들웨어에서 확인
function authenticate(req, res, next) {
const token = req.headers.authorization?.substring(7);
if (tokenBlacklist.has(token)) {
return res.status(401).json({ error: '무효화된 토큰입니다' });
}
// 토큰 검증...
}
문제 2: 세션 스토어 메모리 누수
// ❌ 메모리 스토어 (프로덕션 부적합)
app.use(session({
secret: 'secret',
resave: false,
saveUninitialized: false
// store 설정 없음 → 메모리에 저장
}));
// ✅ 영구 스토어 사용
const MongoStore = require('connect-mongo');
app.use(session({
secret: 'secret',
resave: false,
saveUninitialized: false,
store: MongoStore.create({
mongoUrl: 'mongodb://localhost:27017/mydb'
})
}));
문제 3: 비밀번호 평문 저장
// ❌ 절대 안 됨!
const user = await User.create({
email: '[email protected]',
password: 'mypassword123' // 평문 저장
});
// ✅ 해싱하여 저장
const hashedPassword = await bcrypt.hash('mypassword123', 10);
const user = await User.create({
email: '[email protected]',
password: hashedPassword
});
9. 실전 팁
JWT vs 세션 선택
| 특징 | JWT | 세션 |
|---|---|---|
| 저장 위치 | 클라이언트 | 서버 |
| 확장성 | ✅ 좋음 | ⭕ 세션 스토어 필요 |
| 무효화 | ❌ 어려움 | ✅ 쉬움 |
| 크기 | 큼 (모든 요청) | 작음 (세션 ID만) |
| 사용 사례 | API, 마이크로서비스 | 웹 앱, 단일 서버 |
비밀번호 정책
function validatePassword(password) {
const errors = [];
if (password.length < 8) {
errors.push('8자 이상이어야 합니다');
}
if (!/[A-Z]/.test(password)) {
errors.push('대문자가 포함되어야 합니다');
}
if (!/[a-z]/.test(password)) {
errors.push('소문자가 포함되어야 합니다');
}
if (!/[0-9]/.test(password)) {
errors.push('숫자가 포함되어야 합니다');
}
if (!/[@$!%*?&#]/.test(password)) {
errors.push('특수문자가 포함되어야 합니다');
}
return errors;
}
// 사용
app.post('/auth/register', async (req, res) => {
const { password } = req.body;
const errors = validatePassword(password);
if (errors.length > 0) {
return res.status(400).json({ errors });
}
// 회원가입 계속...
});
환경 변수 관리
# .env
JWT_SECRET=your-super-secret-jwt-key-change-this-in-production
REFRESH_SECRET=your-refresh-token-secret
SESSION_SECRET=your-session-secret
GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-client-secret
require('dotenv').config();
const config = {
jwtSecret: process.env.JWT_SECRET,
refreshSecret: process.env.REFRESH_SECRET,
sessionSecret: process.env.SESSION_SECRET
};
if (!config.jwtSecret || !config.refreshSecret) {
throw new Error('JWT_SECRET과 REFRESH_SECRET이 필요합니다');
}
module.exports = config;
정리
핵심 요약
- 비밀번호 해싱: bcrypt 사용, 절대 평문 저장 금지
- JWT: Stateless, Access Token + Refresh Token
- 세션: Stateful, 서버에서 제어 가능
- OAuth 2.0: 소셜 로그인, Passport.js
- 보안: Helmet, Rate Limiting, CORS, 입력 검증
- 에러 처리: 명확한 에러 메시지, 로깅
인증 방식 비교
| 특징 | JWT | 세션 | OAuth |
|---|---|---|---|
| 복잡도 | 중간 | 낮음 | 높음 |
| 확장성 | ✅ | ⭕ | ✅ |
| 무효화 | ❌ | ✅ | ✅ |
| 사용 사례 | API | 웹 앱 | 소셜 로그인 |
보안 체크리스트
- 비밀번호 해싱 (bcrypt, argon2)
- HTTPS 사용
- JWT Secret 안전하게 관리
- Rate Limiting 적용
- CORS 설정
- Helmet 사용
- 입력 검증
- SQL Injection 방지
- XSS 방지
- CSRF 방지 (웹 앱)
다음 단계
- Node.js 테스트
- Node.js 배포
- Node.js 성능 최적화
추천 학습 자료
공식 문서:
패키지:
관련 글
- C++ REST API 클라이언트 완벽 가이드 | CRUD·인증·에러 처리·프로덕션 패턴 [#21-3]
- C++ SSL/TLS 보안 통신 | OpenSSL과 Asio 연동 완벽 가이드 [#30-2]
- C++ 채팅 서버 만들기 | 다중 클라이언트와 메시지 브로드캐스트 완벽 가이드 [#31-1]
- C++ 보안 코딩 가이드: 오버플로우 방지와 암호화 라이브러리(OpenSSL) 실전 연동 [#43-2]
- C++ 채팅 서버 완성하기 | 인증·방 관리·메시지 히스토리 구현 [#50-1]