[2026] Node.js 인증과 보안 | JWT, bcrypt, 세션, OAuth

[2026] Node.js 인증과 보안 | JWT, bcrypt, 세션, OAuth

이 글의 핵심

Node.js 인증과 보안: JWT, bcrypt, 세션, OAuth. 비밀번호 해싱 (bcrypt)·JWT (JSON Web Token).

들어가며

인증 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

기본 사용법

다음은 javascript를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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();

회원가입 구현

다음은 javascript를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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 생성 및 검증

다음은 javascript를 활용한 상세한 구현 코드입니다. 함수를 통해 로직을 구현합니다, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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: 'hong@example.com' });
console.log('토큰:', token);
const decoded = verifyToken(token);
console.log('디코딩:', decoded);
// { id: 123, email: 'hong@example.com', iat: 1234567890, exp: 1234571490 }

로그인 구현

다음은 javascript를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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 });
    }
});

인증 미들웨어

다음은 javascript를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 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 };

아래 코드는 javascript를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// 사용
const { authenticate } = require('./middlewares/auth');
app.get('/api/profile', authenticate, (req, res) => {
    res.json({ user: req.user });
});

권한 확인 미들웨어

다음은 javascript를 활용한 상세한 구현 코드입니다. 함수를 통해 로직을 구현합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 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 };

다음은 javascript를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 사용
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

구현

다음은 javascript를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 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);

다음은 javascript를 활용한 상세한 구현 코드입니다. 함수를 통해 로직을 구현합니다, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 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

기본 설정

다음은 javascript를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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'
    }
}));

세션 사용

다음은 javascript를 활용한 상세한 구현 코드입니다. 함수를 통해 로직을 구현합니다, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 로그인
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

다음은 javascript를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 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;

다음은 javascript를 활용한 상세한 구현 코드입니다. 함수를 통해 로직을 구현합니다, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 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

다음은 javascript를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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

다음은 javascript를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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

다음은 javascript를 활용한 상세한 구현 코드입니다. 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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

다음은 javascript를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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 방지

다음은 javascript를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// ❌ 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

다음은 javascript를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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. 실전 프로젝트: 완전한 인증 시스템

다음은 javascript를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 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);

다음은 javascript를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 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는 서버에서 무효화할 수 없음 해결: 다음은 javascript를 활용한 상세한 구현 코드입니다. 함수를 통해 로직을 구현합니다, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 블랙리스트 방식
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: 세션 스토어 메모리 누수

다음은 javascript를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// ❌ 메모리 스토어 (프로덕션 부적합)
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: 비밀번호 평문 저장

아래 코드는 javascript를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// ❌ 절대 안 됨!
const user = await User.create({
    email: 'hong@example.com',
    password: 'mypassword123'  // 평문 저장
});
// ✅ 해싱하여 저장
const hashedPassword = await bcrypt.hash('mypassword123', 10);
const user = await User.create({
    email: 'hong@example.com',
    password: hashedPassword
});

9. 실전 팁

JWT vs 세션 선택

특징JWT세션
저장 위치클라이언트서버
확장성✅ 좋음⭕ 세션 스토어 필요
무효화❌ 어려움✅ 쉬움
크기큼 (모든 요청)작음 (세션 ID만)
사용 사례API, 마이크로서비스웹 앱, 단일 서버

비밀번호 정책

다음은 javascript를 활용한 상세한 구현 코드입니다. 함수를 통해 로직을 구현합니다, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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 });
    }
    
    // 회원가입 계속...
});

환경 변수 관리

아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

# .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

아래 코드는 javascript를 사용한 구현 예제입니다. 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 실행 예제
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;

정리

핵심 요약

  1. 비밀번호 해싱: bcrypt 사용, 절대 평문 저장 금지
  2. JWT: Stateless, Access Token + Refresh Token
  3. 세션: Stateful, 서버에서 제어 가능
  4. OAuth 2.0: 소셜 로그인, Passport.js
  5. 보안: Helmet, Rate Limiting, CORS, 입력 검증
  6. 에러 처리: 명확한 에러 메시지, 로깅

인증 방식 비교

특징JWT세션OAuth
복잡도중간낮음높음
확장성
무효화
사용 사례API웹 앱소셜 로그인

보안 체크리스트

  • 비밀번호 해싱 (bcrypt, argon2)
  • HTTPS 사용
  • JWT Secret 안전하게 관리
  • Rate Limiting 적용
  • CORS 설정
  • Helmet 사용
  • 입력 검증
  • SQL Injection 방지
  • XSS 방지
  • CSRF 방지 (웹 앱)

다음 단계

추천 학습 자료

공식 문서:


관련 글

... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3