웹 보안 완전 정복: JWT부터 OAuth 2.0까지 실전 가이드
WEB 스터디에서 배운 보안 기술들로 구축하는 안전한 웹 애플리케이션
웹 보안 완전 정복: JWT부터 OAuth 2.0까지 실전 가이드 🔐
안녕하세요! 오늘은 웹 보안의 핵심 기술들을 스터디하면서 배운 내용을 정리해서 공유하려고 합니다.
지원, 병국, 채민, 호인이와 함께한 WEB 스터디에서 정말 많은 걸 배웠는데, 특히 보안 부분이 완전 꿀잼이었어요! 🍯
🎯 왜 웹 보안이 중요한가?
💥 보안 취약점의 현실
❌ 평문 비밀번호 저장❌ 세션 하이재킹 취약점❌ CSRF 공격 가능성❌ XSS 스크립트 삽입❌ 인증 없는 API 엔드포인트✅ 보안이 잘 구현된 시스템
✅ 토큰 기반 인증 시스템✅ 안전한 쿠키 설정✅ 권한 기반 접근 제어✅ 보안 헤더 적용✅ 입력값 검증 및 이스케이프🔑 1. JWT (JSON Web Token) 완전 분석
JWT란 무엇인가?
const jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
// Header{ "alg": "HS256", "typ": "JWT"}
// Payload{ "sub": "1234567890", "name": "John Doe", "iat": 1516239022, "exp": 1516242622}JWT 생성 및 검증 (Node.js)
const jwt = require("jsonwebtoken");
// JWT 생성function generateToken(user) { const payload = { id: user.id, email: user.email, role: user.role, };
return jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: "1h", });}
// JWT 검증function verifyToken(token) { try { return jwt.verify(token, process.env.JWT_SECRET); } catch (error) { throw new Error("Invalid token"); }}
// 미들웨어로 활용function authMiddleware(req, res, next) { const token = req.headers.authorization?.split(" ")[1];
if (!token) { return res.status(401).json({ error: "No token provided" }); }
try { const decoded = verifyToken(token); req.user = decoded; next(); } catch (error) { return res.status(401).json({ error: "Invalid token" }); }}🍪 2. Cookie 보안 설정 완벽 가이드
안전한 쿠키 설정
// Express.js에서 안전한 쿠키 설정app.use( session({ secret: process.env.SESSION_SECRET, resave: false, saveUninitialized: false, cookie: { secure: true, // HTTPS에서만 전송 httpOnly: true, // JavaScript 접근 차단 maxAge: 3600000, // 1시간 (밀리초) sameSite: "strict", // CSRF 공격 방지 }, }));
// 쿠키 설정 함수function setSecureCookie(res, name, value, options = {}) { const defaultOptions = { httpOnly: true, secure: process.env.NODE_ENV === "production", sameSite: "strict", maxAge: 24 * 60 * 60 * 1000, // 24시간 };
res.cookie(name, value, { ...defaultOptions, ...options });}쿠키 보안 속성 상세 분석
// 보안 속성별 설명const cookieOptions = { // 1. HttpOnly: XSS 공격 방지 httpOnly: true,
// 2. Secure: HTTPS에서만 전송 secure: process.env.NODE_ENV === "production",
// 3. SameSite: CSRF 공격 방지 sameSite: "strict", // 'lax' or 'none' 옵션도 있음
// 4. Path: 쿠키 적용 경로 제한 path: "/",
// 5. Domain: 쿠키 적용 도메인 제한 domain: ".yourdomain.com",
// 6. MaxAge: 쿠키 만료 시간 (초) maxAge: 3600,};🔄 3. Refresh Token 전략
Access Token + Refresh Token 패턴
// 토큰 생성 함수function generateTokens(user) { const accessToken = jwt.sign( { id: user.id, email: user.email }, process.env.ACCESS_TOKEN_SECRET, { expiresIn: "15m" } // 15분 );
const refreshToken = jwt.sign( { id: user.id }, process.env.REFRESH_TOKEN_SECRET, { expiresIn: "7d" } // 7일 );
return { accessToken, refreshToken };}
// 토큰 갱신 엔드포인트app.post("/auth/refresh", async (req, res) => { const { refreshToken } = req.body;
if (!refreshToken) { return res.status(401).json({ error: "Refresh token required" }); }
try { const decoded = jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET); const user = await User.findById(decoded.id);
if (!user || user.refreshToken !== refreshToken) { return res.status(403).json({ error: "Invalid refresh token" }); }
const tokens = generateTokens(user);
// 새로운 refresh token을 DB에 저장 user.refreshToken = tokens.refreshToken; await user.save();
res.json(tokens); } catch (error) { res.status(403).json({ error: "Invalid refresh token" }); }});안전한 토큰 저장 전략
// 클라이언트 사이드 토큰 관리class TokenManager { constructor() { this.accessToken = null; this.refreshToken = null; }
// 토큰 저장 (HttpOnly 쿠키 권장) setTokens(accessToken, refreshToken) { this.accessToken = accessToken; // Refresh Token은 HttpOnly 쿠키에 저장 document.cookie = `refreshToken=${refreshToken}; HttpOnly; Secure; SameSite=Strict`; }
// 자동 토큰 갱신 async refreshAccessToken() { try { const response = await fetch("/auth/refresh", { method: "POST", credentials: "include", // 쿠키 포함 });
const data = await response.json(); this.accessToken = data.accessToken;
return this.accessToken; } catch (error) { // 로그아웃 처리 this.logout(); } }}🛡 4. Protected Route 구현
React에서 Protected Route 구현
import React from "react";import { Navigate, useLocation } from "react-router-dom";import { useAuth } from "../hooks/useAuth";
const ProtectedRoute = ({ children, requiredRole = null }) => { const { user, isAuthenticated, isLoading } = useAuth(); const location = useLocation();
if (isLoading) { return <div>Loading...</div>; }
if (!isAuthenticated) { // 로그인 후 원래 페이지로 리다이렉트하기 위해 state 저장 return <Navigate to="/login" state={{ from: location }} replace />; }
if (requiredRole && user.role !== requiredRole) { return <Navigate to="/unauthorized" replace />; }
return children;};
// 사용 예시function App() { return ( <BrowserRouter> <Routes> <Route path="/login" element={<Login />} /> <Route path="/dashboard" element={ <ProtectedRoute> <Dashboard /> </ProtectedRoute> } /> <Route path="/admin" element={ <ProtectedRoute requiredRole="admin"> <AdminPanel /> </ProtectedRoute> } /> </Routes> </BrowserRouter> );}서버 사이드 Route 보호
// Express.js 라우트 보호 미들웨어function requireAuth(req, res, next) { const token = req.headers.authorization?.split(" ")[1];
if (!token) { return res.status(401).json({ error: "Authentication required" }); }
try { const decoded = jwt.verify(token, process.env.JWT_SECRET); req.user = decoded; next(); } catch (error) { return res.status(401).json({ error: "Invalid token" }); }}
function requireRole(role) { return (req, res, next) => { if (req.user.role !== role) { return res.status(403).json({ error: "Insufficient permissions" }); } next(); };}
// 사용 예시app.get("/api/users", requireAuth, (req, res) => { // 인증된 사용자만 접근 가능});
app.delete("/api/users/:id", requireAuth, requireRole("admin"), (req, res) => { // 관리자만 접근 가능});🔐 5. OAuth 2.0 구현
OAuth 2.0 플로우 구현
// Google OAuth 2.0 구현 예시const passport = require("passport");const GoogleStrategy = require("passport-google-oauth20").Strategy;
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) { return done(null, 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 (error) { done(error, null); } } ));
// 라우트 설정app.get("/auth/google", passport.authenticate("google", { scope: ["profile", "email"] }));
app.get( "/auth/google/callback", passport.authenticate("google", { failureRedirect: "/login" }), (req, res) => { // 성공 시 JWT 생성 및 리다이렉트 const token = generateToken(req.user); res.redirect(`${process.env.CLIENT_URL}?token=${token}`); });커스텀 OAuth 2.0 서버 구현
// Authorization Code Grant 구현app.get("/oauth/authorize", (req, res) => { const { client_id, redirect_uri, response_type, scope, state } = req.query;
// 클라이언트 검증 const client = clients.find((c) => c.id === client_id); if (!client || !client.redirectUris.includes(redirect_uri)) { return res.status(400).json({ error: "invalid_client" }); }
// 사용자에게 권한 승인 요청 res.render("authorize", { client_id, redirect_uri, scope, state, });});
app.post("/oauth/token", async (req, res) => { const { grant_type, code, client_id, client_secret, redirect_uri } = req.body;
if (grant_type === "authorization_code") { // 인증 코드 검증 const authCode = await AuthCode.findOne({ code });
if (!authCode || authCode.expiresAt < new Date()) { return res.status(400).json({ error: "invalid_grant" }); }
// 클라이언트 검증 const client = await Client.findOne({ id: client_id, secret: client_secret, });
if (!client) { return res.status(400).json({ error: "invalid_client" }); }
// 토큰 생성 const tokens = generateTokens(authCode.user);
res.json({ access_token: tokens.accessToken, refresh_token: tokens.refreshToken, token_type: "Bearer", expires_in: 3600, }); }});🔒 보안 베스트 프랙티스
1️⃣ 환경변수 관리
// .env 파일JWT_SECRET=your-super-secret-jwt-key-minimum-32-charactersREFRESH_TOKEN_SECRET=your-super-secret-refresh-key-minimum-32-charactersSESSION_SECRET=your-super-secret-session-key-minimum-32-charactersDB_CONNECTION_STRING=your-database-connection-string
// config.jsconst config = { jwt: { secret: process.env.JWT_SECRET, expiresIn: process.env.JWT_EXPIRES_IN || '1h' }, refresh: { secret: process.env.REFRESH_TOKEN_SECRET, expiresIn: process.env.REFRESH_EXPIRES_IN || '7d' }};2️⃣ 보안 헤더 설정
const helmet = require("helmet");const rateLimit = require("express-rate-limit");
// 보안 헤더 설정app.use( helmet({ contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], styleSrc: ["'self'", "'unsafe-inline'"], scriptSrc: ["'self'"], imgSrc: ["'self'", "data:", "https:"], }, }, }));
// Rate Limitingconst limiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15분 max: 100, // 최대 100 요청 message: "Too many requests from this IP",});
app.use("/api/", limiter);3️⃣ 입력값 검증
const joi = require("joi");
// 입력값 검증 스키마const loginSchema = joi.object({ email: joi.string().email().required(), password: joi.string().min(8).required(),});
const registerSchema = joi.object({ email: joi.string().email().required(), password: joi .string() .min(8) .pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/) .required(), name: joi.string().min(2).max(50).required(),});
// 검증 미들웨어function validate(schema) { return (req, res, next) => { const { error } = schema.validate(req.body); if (error) { return res.status(400).json({ error: error.details[0].message, }); } next(); };}📊 실제 적용 결과
보안 테스트 결과
🔒 JWT 토큰 탈취 방지: ✅🔒 CSRF 공격 차단: ✅🔒 XSS 스크립트 차단: ✅🔒 세션 하이재킹 방지: ✅🔒 무차별 대입 공격 차단: ✅🔒 SQL 인젝션 방지: ✅성능 영향 분석
// 보안 기능별 성능 오버헤드const performanceMetrics = { jwtVerification: "< 1ms", cookieValidation: "< 0.5ms", rateLimiting: "< 0.1ms", inputValidation: "< 2ms", totalOverhead: "< 4ms per request",};🎉 마무리
웹 보안은 한 번 설정하고 끝나는 게 아니라 지속적으로 관리해야 하는 영역입니다.
💡 핵심 포인트
- JWT + Refresh Token 조합으로 안전한 인증
- HttpOnly, Secure 쿠키 설정으로 XSS/CSRF 방지
- Protected Route로 권한 기반 접근 제어
- OAuth 2.0으로 소셜 로그인 구현
- 입력값 검증 및 Rate Limiting으로 추가 보안
스터디를 통해 배운 내용들을 실제 프로젝트에 적용해보니 보안 의식이 많이 향상되었어요! 🛡️
여러분은 어떤 웹 보안 기술을 가장 중요하게 생각하시나요? 실제 프로젝트에서 겪은 보안 이슈가 있다면 댓글로 공유해주세요! 💬
다음 글에서는 고급 보안 기법과 해킹 시도 대응 방법에 대해 다뤄보겠습니다! 🚀
💬 댓글