웹 보안 완전 정복: 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으로 추가 보안

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


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

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

자주 묻는 질문

jwt.sign()으로 payload와 secret을 사용해 토큰을 생성하고, jwt.verify()로 토큰의 유효성을 검증합니다. 만료 시간은 expiresIn 옵션으로 설정합니다.
httpOnly(XSS 방지), secure(HTTPS 전용), sameSite(CSRF 방지) 3가지가 필수입니다. 프로덕션에서는 반드시 모두 설정해야 합니다.
Access Token은 15분 정도의 짧은 유효기간으로 API 요청에 사용하고, Refresh Token은 7일 정도의 긴 유효기간으로 Access Token을 갱신하는 데 사용합니다. 보안과 편의성의 균형을 맞춥니다.
사용자 인증 → 인가 코드 발급 → 코드로 액세스 토큰 요청 → 토큰으로 리소스 접근하는 4단계입니다. 가장 보안이 강력한 방식입니다.

💬 댓글