웹 보안 완전 정복: JWT부터 OAuth 2.0까지 실전 가이드웹 보안 완전 정복: JWT부터 OAuth 2.0까지 실전 가이드

웹 보안 완전 정복: JWT부터 OAuth 2.0까지 실전 가이드

WEB 스터디에서 배운 보안 기술들로 구축하는 안전한 웹 애플리케이션

웹 보안 완전 정복: JWT부터 OAuth 2.0까지 실전 가이드 🔐

안녕하세요! 오늘은 웹 보안의 핵심 기술들을 스터디하면서 배운 내용을 정리해서 공유하려고 합니다.

지원, 병국, 채민, 호인이와 함께한 WEB 스터디에서 정말 많은 걸 배웠는데, 특히 보안 부분이 완전 꿀잼이었어요! 🍯

🎯 왜 웹 보안이 중요한가?

💥 보안 취약점의 현실

❌ 평문 비밀번호 저장
❌ 세션 하이재킹 취약점
❌ CSRF 공격 가능성
❌ XSS 스크립트 삽입
❌ 인증 없는 API 엔드포인트

✅ 보안이 잘 구현된 시스템

✅ 토큰 기반 인증 시스템
✅ 안전한 쿠키 설정
✅ 권한 기반 접근 제어
✅ 보안 헤더 적용
✅ 입력값 검증 및 이스케이프

🔑 1. JWT (JSON Web Token) 완전 분석

JWT란 무엇인가?

Header.Payload.Signature
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" });
}
}

안전한 쿠키 설정

// 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-characters
REFRESH_TOKEN_SECRET=your-super-secret-refresh-key-minimum-32-characters
SESSION_SECRET=your-super-secret-session-key-minimum-32-characters
DB_CONNECTION_STRING=your-database-connection-string
// config.js
const 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 Limiting
const 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으로 추가 보안

스터디를 통해 배운 내용들을 실제 프로젝트에 적용해보니 보안 의식이 많이 향상되었어요! 🛡️


여러분은 어떤 웹 보안 기술을 가장 중요하게 생각하시나요? 실제 프로젝트에서 겪은 보안 이슈가 있다면 댓글로 공유해주세요! 💬

다음 글에서는 고급 보안 기법과 해킹 시도 대응 방법에 대해 다뤄보겠습니다! 🚀

💬 댓글