์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐Ÿ›ก๏ธ CSRF ๋ฐฉ์–ด ํ† ํฐ ์ƒ์„ฑ ์†Œ์Šค์ฝ”๋“œ ์™„๋ฒฝ ๊ฐ€์ด๋“œ

๐Ÿ›ก๏ธ CSRF ๋ฐฉ์–ด ํ† ํฐ ์ƒ์„ฑ ์†Œ์Šค์ฝ”๋“œ ์™„๋ฒฝ ๊ฐ€์ด๋“œ

์›น ๋ณด์•ˆ์˜ ํ•„์ˆ˜ํ…œ! CSRF ๊ณต๊ฒฉ ๋ง‰๋Š” ํ† ํฐ ์ƒ์„ฑ ๋ฐฉ๋ฒ• ์ด์ •๋ฆฌ ใ…‹ใ…‹ใ…‹

๐ŸŽฏ CSRF๊ฐ€ ๋ญ”๋ฐ ์ด๋ ‡๊ฒŒ ๋‚œ๋ฆฌ์•ผ?

์•ผ ์†”์งํžˆ ๋งํ•ด์„œ ์›น ๊ฐœ๋ฐœํ•˜๋‹ค๋ณด๋ฉด ๋ณด์•ˆ ์ด์Šˆ ์ง„์งœ ๊ฐœ๋งŽ์ž–์•„ ใ…‹ใ…‹ใ…‹ ๊ทธ ์ค‘์—์„œ๋„ CSRF(Cross-Site Request Forgery)๋Š” ์ง„์งœ ๋ฌด์‹œํ•˜๋ฉด ํฐ์ผ๋‚˜๋Š” ๋…€์„์ด์•ผ.

๊ฐ„๋‹จํ•˜๊ฒŒ ์„ค๋ช…ํ•˜๋ฉด, ์‚ฌ์šฉ์ž๊ฐ€ ์˜๋„ํ•˜์ง€ ์•Š์€ ์š”์ฒญ์„ ๊ณต๊ฒฉ์ž๊ฐ€ ๋ชฐ๋ž˜ ๋ณด๋‚ด๊ฒŒ ๋งŒ๋“œ๋Š” ๊ณต๊ฒฉ ๋ฐฉ์‹์ด๊ฑฐ๋“ ? ์˜ˆ๋ฅผ ๋“ค์–ด ๋„ค๊ฐ€ ์€ํ–‰ ์‚ฌ์ดํŠธ์— ๋กœ๊ทธ์ธํ•œ ์ƒํƒœ์—์„œ ์•…์„ฑ ๋งํฌ๋ฅผ ํด๋ฆญํ•˜๋ฉด, ๋„ˆ๋„ ๋ชจ๋ฅด๊ฒŒ ๋ˆ์ด ์ด์ฒด๋  ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฑฐ์ง€ ใ„ทใ„ทใ„ท

์ด๋Ÿฐ ๊ณต๊ฒฉ์„ ๋ง‰๊ธฐ ์œ„ํ•ด ๋‚˜์˜จ ๊ฒŒ ๋ฐ”๋กœ CSRF ํ† ํฐ์ด์•ผ! ํ† ํฐ์€ ์ผ์ข…์˜ ๋น„๋ฐ€ ์•”ํ˜ธ ๊ฐ™์€ ๊ฑด๋ฐ, ์„œ๋ฒ„๊ฐ€ ์ƒ์„ฑํ•ด์„œ ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ์ฃผ๊ณ , ํด๋ผ์ด์–ธํŠธ๋Š” ์š”์ฒญํ•  ๋•Œ๋งˆ๋‹ค ์ด ํ† ํฐ์„ ๊ฐ™์ด ๋ณด๋‚ด๋Š” ๊ฑฐ์ง€. ์„œ๋ฒ„๋Š” ์ด ํ† ํฐ์„ ๊ฒ€์ฆํ•ด์„œ "์•„ ์ด๊ฑด ์ง„์งœ ์šฐ๋ฆฌ ์‚ฌ์šฉ์ž๊ฐ€ ๋ณด๋‚ธ ์š”์ฒญ์ด๊ตฌ๋‚˜" ํ•˜๊ณ  ํ™•์ธํ•˜๋Š” ๊ฑฐ์•ผ.

๐Ÿ’ก CSRF ๊ณต๊ฒฉ ์‹œ๋‚˜๋ฆฌ์˜ค ์˜ˆ์‹œ 1. ์‚ฌ์šฉ์ž๊ฐ€ A์€ํ–‰ ์‚ฌ์ดํŠธ์— ๋กœ๊ทธ์ธ (์„ธ์…˜ ์ฟ ํ‚ค ์ƒ์„ฑ๋จ)
2. ๊ณต๊ฒฉ์ž๊ฐ€ ๋งŒ๋“  ์•…์„ฑ ์‚ฌ์ดํŠธ B๋ฅผ ๋ฐฉ๋ฌธ
3. ์‚ฌ์ดํŠธ B์— ์ˆจ๊ฒจ์ง„ ํผ์ด ์ž๋™์œผ๋กœ A์€ํ–‰์— ์†ก๊ธˆ ์š”์ฒญ ์ „์†ก
4. ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ์ž๋™์œผ๋กœ A์€ํ–‰ ์ฟ ํ‚ค๋ฅผ ํ•จ๊ป˜ ์ „์†ก
5. A์€ํ–‰ ์„œ๋ฒ„๋Š” ์ •์ƒ ์š”์ฒญ์œผ๋กœ ์ฐฉ๊ฐํ•˜๊ณ  ์†ก๊ธˆ ์ฒ˜๋ฆฌ ๐Ÿ˜ฑ
CSRF ํ† ํฐ ๋ฐฉ์–ด ๋ฉ”์ปค๋‹ˆ์ฆ˜ ์‚ฌ์šฉ์ž ๋ธŒ๋ผ์šฐ์ € ๐Ÿ›ก๏ธ CSRF ํ† ํฐ ๊ฒ€์ฆ ์‹œ์Šคํ…œ ์„œ๋ฒ„ ํ† ํฐ ์ƒ์„ฑ ํ† ํฐ ๊ฒ€์ฆ ์š”์ฒญ ์ฒ˜๋ฆฌ โ‘  ํ† ํฐ ์š”์ฒญ โ‘ก ํ† ํฐ ๋ฐœ๊ธ‰ โ‘ข ํ† ํฐ ์ „๋‹ฌ โ‘ฃ ํ† ํฐ ์ €์žฅ โœ… ํ† ํฐ์ด ์ผ์น˜ํ•˜๋ฉด ์š”์ฒญ ์ฒ˜๋ฆฌ โŒ ํ† ํฐ์ด ์—†๊ฑฐ๋‚˜ ํ‹€๋ฆฌ๋ฉด ์š”์ฒญ ๊ฑฐ๋ถ€

๐Ÿ” CSRF ํ† ํฐ์˜ ํ•ต์‹ฌ ์›๋ฆฌ

CSRF ํ† ํฐ์ด ์ž‘๋™ํ•˜๋Š” ์›๋ฆฌ๋Š” ์ƒ๊ฐ๋ณด๋‹ค ์‹ฌํ”Œํ•ด ใ…‹ใ…‹ ๊ทผ๋ฐ ์ด๊ฒŒ ์ง„์งœ ํšจ๊ณผ์ ์ด๊ฑฐ๋“ ?

๊ธฐ๋ณธ ์•„์ด๋””์–ด๋Š” ์ด๊ฑฐ์•ผ: ์˜ˆ์ธก ๋ถˆ๊ฐ€๋Šฅํ•œ ๋žœ๋ค ๊ฐ’์„ ์ƒ์„ฑํ•ด์„œ ์„œ๋ฒ„์™€ ํด๋ผ์ด์–ธํŠธ๋งŒ ๊ณต์œ ํ•˜๋Š” ๊ฑฐ์ง€. ๊ณต๊ฒฉ์ž๋Š” ์ด ๊ฐ’์„ ์•Œ ์ˆ˜ ์—†์œผ๋‹ˆ๊นŒ ์ •์ƒ์ ์ธ ์š”์ฒญ์„ ์œ„์กฐํ•  ์ˆ˜ ์—†๊ฒŒ ๋˜๋Š” ์›๋ฆฌ์•ผ.

ํ† ํฐ์€ ๋ณดํ†ต ์„ธ์…˜๋งˆ๋‹ค ๋˜๋Š” ์š”์ฒญ๋งˆ๋‹ค ์ƒˆ๋กœ ์ƒ์„ฑ๋˜๊ณ , ์„œ๋ฒ„ ์ธก์—์„œ๋Š” ์ด ํ† ํฐ์„ ์„ธ์…˜์ด๋‚˜ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ €์žฅํ•ด๋‘ฌ. ๊ทธ๋ฆฌ๊ณ  ํด๋ผ์ด์–ธํŠธ๊ฐ€ ํผ์„ ์ œ์ถœํ•˜๊ฑฐ๋‚˜ API ์š”์ฒญ์„ ๋ณด๋‚ผ ๋•Œ ์ด ํ† ํฐ์„ ํ•จ๊ป˜ ๋ณด๋‚ด๋ฉด, ์„œ๋ฒ„๋Š” ์ €์žฅ๋œ ํ† ํฐ๊ณผ ๋น„๊ตํ•ด์„œ ๊ฒ€์ฆํ•˜๋Š” ๊ฑฐ์ง€.

๐ŸŽ“ ํ† ํฐ ์ƒ์„ฑ ์‹œ ๊ณ ๋ ค์‚ฌํ•ญ 1. ์ถฉ๋ถ„ํ•œ ์—”ํŠธ๋กœํ”ผ: ํ† ํฐ์€ ์ตœ์†Œ 128๋น„ํŠธ ์ด์ƒ์˜ ๋žœ๋ค์„ฑ์„ ๊ฐ€์ ธ์•ผ ํ•ด. ์˜ˆ์ธก ๊ฐ€๋Šฅํ•˜๋ฉด ์˜๋ฏธ ์—†๊ฑฐ๋“  ใ…‹ใ…‹

2. ์•”ํ˜ธํ•™์ ์œผ๋กœ ์•ˆ์ „ํ•œ ๋‚œ์ˆ˜ ์ƒ์„ฑ๊ธฐ: Math.random() ๊ฐ™์€ ๊ฑฐ ์“ฐ๋ฉด ์•ˆ ๋ผ! crypto ๋ชจ๋“ˆ ์จ์•ผ ํ•จ.

3. ํ† ํฐ ์œ ํšจ๊ธฐ๊ฐ„: ๋„ˆ๋ฌด ๊ธธ๋ฉด ์œ„ํ—˜ํ•˜๊ณ , ๋„ˆ๋ฌด ์งง์œผ๋ฉด ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์ด ๋‚˜๋น ์ ธ. ๋ณดํ†ต ์„ธ์…˜ ํƒ€์ž„์•„์›ƒ๊ณผ ๋™์ผํ•˜๊ฒŒ ์„ค์ •.

4. ํ† ํฐ ์ €์žฅ ์œ„์น˜: ์„œ๋ฒ„ ์ธก ์„ธ์…˜, ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค, ๋˜๋Š” ์•”ํ˜ธํ™”๋œ ์ฟ ํ‚ค์— ์ €์žฅ ๊ฐ€๋Šฅ.

๐Ÿ’ป Node.js + Express์—์„œ CSRF ํ† ํฐ ์ƒ์„ฑํ•˜๊ธฐ

์ž ์ด์ œ ๋ณธ๊ฒฉ์ ์œผ๋กœ ์ฝ”๋“œ ๋“ค์–ด๊ฐ„๋‹ค ใ…‹ใ…‹ใ…‹ Node.js ํ™˜๊ฒฝ์—์„œ ๊ฐ€์žฅ ๋งŽ์ด ์“ฐ๋Š” ๋ฐฉ๋ฒ•๋ถ€ํ„ฐ ๋ณผ๊ฒŒ.

Express์—์„œ๋Š” csurf ๋ฏธ๋“ค์›จ์–ด๊ฐ€ ์ „ํ†ต์ ์œผ๋กœ ๋งŽ์ด ์“ฐ์˜€๋Š”๋ฐ, ์ตœ๊ทผ์—๋Š” ๋” ํ˜„๋Œ€์ ์ธ ๋ฐฉ๋ฒ•๋“ค๋„ ๋งŽ์ด ๋‚˜์™”์–ด. ๊ทผ๋ฐ ์›๋ฆฌ๋ฅผ ์ดํ•ดํ•˜๋ ค๋ฉด ์ง์ ‘ ๊ตฌํ˜„ํ•ด๋ณด๋Š” ๊ฒŒ ์ตœ๊ณ ์ง€!

๐Ÿ“ ๊ธฐ๋ณธ์ ์ธ CSRF ํ† ํฐ ์ƒ์„ฑ ํ•จ์ˆ˜
const crypto = require('crypto');

// CSRF ํ† ํฐ ์ƒ์„ฑ ํ•จ์ˆ˜
function generateCSRFToken() {
    // 32๋ฐ”์ดํŠธ(256๋น„ํŠธ)์˜ ๋žœ๋ค ๋ฐ์ดํ„ฐ ์ƒ์„ฑ
    return crypto.randomBytes(32).toString('hex');
}

// ์‚ฌ์šฉ ์˜ˆ์‹œ
const token = generateCSRFToken();
console.log('์ƒ์„ฑ๋œ ํ† ํฐ:', token);
// ์ถœ๋ ฅ ์˜ˆ: ์ƒ์„ฑ๋œ ํ† ํฐ: a3f5e8d9c2b1a4f6e7d8c9b0a1f2e3d4c5b6a7f8e9d0c1b2a3f4e5d6c7b8a9f0

์ด๊ฒŒ ๊ฐ€์žฅ ๊ธฐ๋ณธ์ ์ธ ํ˜•ํƒœ์•ผ. crypto.randomBytes()๋Š” ์•”ํ˜ธํ•™์ ์œผ๋กœ ์•ˆ์ „ํ•œ ๋‚œ์ˆ˜๋ฅผ ์ƒ์„ฑํ•ด์ฃผ๊ฑฐ๋“ . ๊ทธ๋ฆฌ๊ณ  hex๋กœ ์ธ์ฝ”๋”ฉํ•˜๋ฉด ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜๋ผ์„œ ๋‹ค๋ฃจ๊ธฐ ํŽธํ•ด์ง ใ…‡ใ…‡

๐Ÿ”ง Express ๋ฏธ๋“ค์›จ์–ด๋กœ ๊ตฌํ˜„ํ•˜๊ธฐ
const express = require('express');
const session = require('express-session');
const crypto = require('crypto');

const app = express();

// ์„ธ์…˜ ์„ค์ •
app.use(session({
    secret: 'your-secret-key-here',
    resave: false,
    saveUninitialized: true,
    cookie: { 
        secure: true, // HTTPS์—์„œ๋งŒ ์ž‘๋™
        httpOnly: true, // JavaScript ์ ‘๊ทผ ์ฐจ๋‹จ
        sameSite: 'strict' // CSRF ์ถ”๊ฐ€ ๋ฐฉ์–ด
    }
}));

// CSRF ํ† ํฐ ์ƒ์„ฑ ๋ฏธ๋“ค์›จ์–ด
app.use((req, res, next) => {
    if (!req.session.csrfToken) {
        req.session.csrfToken = crypto.randomBytes(32).toString('hex');
    }
    // ํ…œํ”Œ๋ฆฟ์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก locals์— ์ €์žฅ
    res.locals.csrfToken = req.session.csrfToken;
    next();
});

// CSRF ํ† ํฐ ๊ฒ€์ฆ ๋ฏธ๋“ค์›จ์–ด
function verifyCSRFToken(req, res, next) {
    const token = req.body.csrfToken || req.headers['x-csrf-token'];
    
    if (!token || token !== req.session.csrfToken) {
        return res.status(403).json({ 
            error: 'CSRF ํ† ํฐ์ด ์œ ํšจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค ใ… ใ… ' 
        });
    }
    
    next();
}

// ํผ ํŽ˜์ด์ง€ ๋ผ์šฐํŠธ
app.get('/form', (req, res) => {
    res.send(`
        <form method="POST" action="/submit">
            <input type="hidden" name="csrfToken" value="${res.locals.csrfToken}">
            <input type="text" name="data" placeholder="๋ฐ์ดํ„ฐ ์ž…๋ ฅ">
            <button type="submit">์ „์†ก</button>
        </form>
    `);
});

// ํผ ์ œ์ถœ ์ฒ˜๋ฆฌ (CSRF ๋ณดํ˜ธ๋จ)
app.post('/submit', verifyCSRFToken, (req, res) => {
    res.json({ 
        success: true, 
        message: '๋ฐ์ดํ„ฐ๊ฐ€ ์•ˆ์ „ํ•˜๊ฒŒ ์ฒ˜๋ฆฌ๋˜์—ˆ์Šต๋‹ˆ๋‹ค! ๐ŸŽ‰' 
    });
});

app.listen(3000, () => {
    console.log('์„œ๋ฒ„ ์‹คํ–‰ ์ค‘: http://localhost:3000');
});
โš ๏ธ ์ฃผ์˜์‚ฌํ•ญ ์œ„ ์ฝ”๋“œ๋Š” ๊ต์œก ๋ชฉ์ ์˜ ๊ฐ„๋‹จํ•œ ์˜ˆ์‹œ์•ผ. ์‹ค์ œ ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ๋Š” ๋” ๋งŽ์€ ๋ณด์•ˆ ์„ค์ •์ด ํ•„์š”ํ•ด:
- HTTPS ํ•„์ˆ˜ ์‚ฌ์šฉ
- ์„ธ์…˜ ์Šคํ† ์–ด๋ฅผ Redis ๊ฐ™์€ ๊ณณ์— ์ €์žฅ
- ํ† ํฐ ๋งŒ๋ฃŒ ์‹œ๊ฐ„ ์„ค์ •
- Rate limiting ์ ์šฉ
- ์—๋Ÿฌ ํ•ธ๋“ค๋ง ๊ฐ•ํ™”

๐ŸŽจ csurf ํŒจํ‚ค์ง€ ์‚ฌ์šฉํ•˜๊ธฐ

์ง์ ‘ ๊ตฌํ˜„ํ•˜๋Š” ๊ฒƒ๋„ ์ข‹์ง€๋งŒ, ๊ฒ€์ฆ๋œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์“ฐ๋Š” ๊ฒŒ ๋” ์•ˆ์ „ํ•  ๋•Œ๊ฐ€ ๋งŽ์•„ ใ…‹ใ…‹ csurf๋Š” Express์šฉ CSRF ๋ณดํ˜ธ ๋ฏธ๋“ค์›จ์–ด์ธ๋ฐ, ์‚ฌ์šฉ๋ฒ•์ด ์ง„์งœ ๊ฐ„๋‹จํ•ด.

๐Ÿ“ฆ csurf ์„ค์น˜ ๋ฐ ๊ธฐ๋ณธ ์‚ฌ์šฉ๋ฒ•
// ์„ค์น˜
// npm install csurf cookie-parser

const express = require('express');
const cookieParser = require('cookie-parser');
const csrf = require('csurf');

const app = express();

// ๋ฏธ๋“ค์›จ์–ด ์„ค์ •
app.use(cookieParser());
app.use(express.urlencoded({ extended: false }));

// CSRF ๋ณดํ˜ธ ์„ค์ • (์ฟ ํ‚ค ๊ธฐ๋ฐ˜)
const csrfProtection = csrf({ cookie: true });

// ๋ชจ๋“  ๋ผ์šฐํŠธ์— ์ ์šฉ
app.use(csrfProtection);

// ํผ ๋ Œ๋”๋ง
app.get('/form', (req, res) => {
    // req.csrfToken()์œผ๋กœ ํ† ํฐ ๊ฐ€์ ธ์˜ค๊ธฐ
    res.send(`
        <form method="POST" action="/process">
            <input type="hidden" name="_csrf" value="${req.csrfToken()}">
            <input type="text" name="username" placeholder="์‚ฌ์šฉ์ž๋ช…">
            <button type="submit">์ œ์ถœ</button>
        </form>
    `);
});

// POST ์š”์ฒญ ์ฒ˜๋ฆฌ (์ž๋™์œผ๋กœ CSRF ๊ฒ€์ฆ๋จ)
app.post('/process', (req, res) => {
    res.send('์ฒ˜๋ฆฌ ์™„๋ฃŒ! โœ…');
});

// CSRF ์—๋Ÿฌ ํ•ธ๋“ค๋ง
app.use((err, req, res, next) => {
    if (err.code === 'EBADCSRFTOKEN') {
        res.status(403).send('CSRF ํ† ํฐ ๊ฒ€์ฆ ์‹คํŒจ ๐Ÿ˜ข');
    } else {
        next(err);
    }
});

app.listen(3000);

csurf์˜ ์žฅ์ ์€ ์ž๋™์œผ๋กœ ํ† ํฐ ์ƒ์„ฑ, ๊ฒ€์ฆ, ์—๋Ÿฌ ์ฒ˜๋ฆฌ๋ฅผ ํ•ด์ค€๋‹ค๋Š” ๊ฑฐ์•ผ. ๊ทผ๋ฐ ์ตœ๊ทผ์—๋Š” ์ด ํŒจํ‚ค์ง€๊ฐ€ deprecated ๋์–ด ใ… ใ…  ๊ทธ๋ž˜์„œ ๋Œ€์•ˆ์„ ์ฐพ์•„์•ผ ํ•˜๋Š”๋ฐ, ๊ทธ๊ฑด ์กฐ๊ธˆ ์žˆ๋‹ค๊ฐ€ ์„ค๋ช…ํ• ๊ฒŒ!

๐Ÿ Python Flask์—์„œ CSRF ํ† ํฐ ๊ตฌํ˜„

Python ๊ฐœ๋ฐœ์ž๋“ค ์—ฌ๊ธฐ ์ฃผ๋ชฉ ใ…‹ใ…‹ใ…‹ Flask์—์„œ๋„ CSRF ๋ณดํ˜ธ๋Š” ํ•„์ˆ˜์•ผ. Flask-WTF ํ™•์žฅ์„ ์“ฐ๋ฉด ์ง„์งœ ์‰ฝ๊ฒŒ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์–ด.

๐Ÿ Flask-WTF๋ฅผ ์ด์šฉํ•œ CSRF ๋ณดํ˜ธ
from flask import Flask, render_template, request, jsonify
from flask_wtf.csrf import CSRFProtect, generate_csrf
import secrets

app = Flask(__name__)

# ์‹œํฌ๋ฆฟ ํ‚ค ์„ค์ • (์‹ค์ œ๋กœ๋Š” ํ™˜๊ฒฝ๋ณ€์ˆ˜์—์„œ ๊ฐ€์ ธ์™€์•ผ ํ•จ)
app.config['SECRET_KEY'] = secrets.token_hex(32)

# CSRF ๋ณดํ˜ธ ํ™œ์„ฑํ™”
csrf = CSRFProtect(app)

@app.route('/form')
def form():
    # ํ…œํ”Œ๋ฆฟ์— ์ž๋™์œผ๋กœ csrf_token() ํ•จ์ˆ˜ ์ œ๊ณต๋จ
    return '''
        <form method="POST" action="/submit">
            <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
            <input type="text" name="data">
            <button type="submit">์ „์†ก</button>
        </form>
    '''

@app.route('/submit', methods=['POST'])
def submit():
    # CSRF ํ† ํฐ์€ ์ž๋™์œผ๋กœ ๊ฒ€์ฆ๋จ
    data = request.form.get('data')
    return jsonify({'success': True, 'data': data})

# API ์—”๋“œํฌ์ธํŠธ์—์„œ CSRF ํ† ํฐ ๊ฐ€์ ธ์˜ค๊ธฐ
@app.route('/api/get-token')
def get_token():
    token = generate_csrf()
    return jsonify({'csrfToken': token})

# ํŠน์ • ๋ผ์šฐํŠธ์—์„œ CSRF ๋ณดํ˜ธ ๋น„ํ™œ์„ฑํ™” (API ๋“ฑ)
@app.route('/api/public', methods=['POST'])
@csrf.exempt
def public_api():
    return jsonify({'message': '๊ณต๊ฐœ API ์—”๋“œํฌ์ธํŠธ'})

if __name__ == '__main__':
    app.run(debug=True)
๐Ÿ”จ ์ˆ˜๋™์œผ๋กœ CSRF ํ† ํฐ ์ƒ์„ฑํ•˜๊ธฐ (Flask)
from flask import Flask, session
import secrets
import hashlib
import hmac

app = Flask(__name__)
app.secret_key = secrets.token_hex(32)

def generate_csrf_token():
    """CSRF ํ† ํฐ ์ƒ์„ฑ"""
    if '_csrf_token' not in session:
        session['_csrf_token'] = secrets.token_urlsafe(32)
    return session['_csrf_token']

def validate_csrf_token(token):
    """CSRF ํ† ํฐ ๊ฒ€์ฆ"""
    return token == session.get('_csrf_token')

# ํ…œํ”Œ๋ฆฟ์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ์ปจํ…์ŠคํŠธ ํ”„๋กœ์„ธ์„œ ๋“ฑ๋ก
@app.context_processor
def inject_csrf_token():
    return dict(csrf_token=generate_csrf_token)

@app.before_request
def csrf_protect():
    """POST ์š”์ฒญ์— ๋Œ€ํ•ด CSRF ํ† ํฐ ๊ฒ€์ฆ"""
    if request.method == "POST":
        token = request.form.get('_csrf_token')
        if not token or not validate_csrf_token(token):
            abort(403, 'CSRF ํ† ํฐ ๊ฒ€์ฆ ์‹คํŒจ!')

# ์‚ฌ์šฉ ์˜ˆ์‹œ
@app.route('/protected-form')
def protected_form():
    return render_template('form.html')

Flask๋Š” ์ง„์งœ ๊ฐ„๋‹จํ•˜์ง€? ใ…‹ใ…‹ Flask-WTF๊ฐ€ ๋Œ€๋ถ€๋ถ„์˜ ์ž‘์—…์„ ์ž๋™์œผ๋กœ ์ฒ˜๋ฆฌํ•ด์ค˜์„œ ๊ฐœ๋ฐœ์ž๋Š” ์‹ ๊ฒฝ ์“ธ ๊ฒŒ ๋ณ„๋กœ ์—†์–ด. ๊ทผ๋ฐ ๋‚ด๋ถ€ ๋™์ž‘ ์›๋ฆฌ๋ฅผ ์ดํ•ดํ•˜๋Š” ๊ฑด ์ค‘์š”ํ•˜๋‹ˆ๊นŒ ์ˆ˜๋™ ๊ตฌํ˜„ ์ฝ”๋“œ๋„ ๊ฐ™์ด ๋ด๋‘ฌ!

โ˜• Java Spring์—์„œ CSRF ํ† ํฐ ์ฒ˜๋ฆฌ

์—”ํ„ฐํ”„๋ผ์ด์ฆˆ ํ™˜๊ฒฝ์—์„œ ๋งŽ์ด ์“ฐ๋Š” Spring๋„ ๋นผ๋†“์„ ์ˆ˜ ์—†์ง€ ใ…‹ใ…‹ Spring Security๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ CSRF ๋ณดํ˜ธ๊ฐ€ ํ™œ์„ฑํ™”๋˜์–ด ์žˆ์–ด. ์˜คํžˆ๋ ค ๋น„ํ™œ์„ฑํ™”ํ•˜๋Š” ๊ฒŒ ๋” ์–ด๋ ค์šธ ์ •๋„ ใ…‹ใ…‹ใ…‹

โ˜• Spring Security CSRF ์„ค์ •
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            // CSRF ๋ณดํ˜ธ ํ™œ์„ฑํ™” (๊ธฐ๋ณธ๊ฐ’)
            .csrf(csrf -> csrf
                // ์ฟ ํ‚ค ๊ธฐ๋ฐ˜ ํ† ํฐ ์ €์žฅ์†Œ ์‚ฌ์šฉ
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
                // ํŠน์ • ๊ฒฝ๋กœ๋Š” CSRF ๋ณดํ˜ธ ์ œ์™ธ
                .ignoringRequestMatchers("/api/public/**")
            )
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/", "/login").permitAll()
                .anyRequest().authenticated()
            );
        
        return http.build();
    }
}
๐ŸŽฏ Thymeleaf ํ…œํ”Œ๋ฆฟ์—์„œ CSRF ํ† ํฐ ์‚ฌ์šฉ
<!-- Thymeleaf๋Š” ์ž๋™์œผ๋กœ CSRF ํ† ํฐ์„ ํผ์— ์ถ”๊ฐ€ํ•จ -->
<form th:action="@{/submit}" method="post">
    <!-- ์ด ๋ถ€๋ถ„์ด ์ž๋™์œผ๋กœ ์ถ”๊ฐ€๋จ:
    <input type="hidden" name="_csrf" value="ํ† ํฐ๊ฐ’"/>
    -->
    <input type="text" name="data"/>
    <button type="submit">์ „์†ก</button>
</form>

<!-- AJAX ์š”์ฒญ์—์„œ CSRF ํ† ํฐ ์‚ฌ์šฉ -->
<script th:inline="javascript">
    const token = /*[[${_csrf.token}]]*/ '';
    const header = /*[[${_csrf.headerName}]]*/ '';
    
    fetch('/api/data', {
        method: 'POST',
        headers: {
            [header]: token,
            'Content-Type': 'application/json'
        },
        body: JSON.stringify({ data: 'test' })
    });
</script>
๐Ÿ”ง ์ปจํŠธ๋กค๋Ÿฌ์—์„œ CSRF ํ† ํฐ ์ ‘๊ทผ
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.web.bind.annotation.*;

@RestController
public class CsrfController {
    
    // CSRF ํ† ํฐ ์ •๋ณด ๋ฐ˜ํ™˜
    @GetMapping("/api/csrf-token")
    public CsrfToken getCsrfToken(CsrfToken token) {
        return token;
    }
    
    // POST ์š”์ฒญ ์ฒ˜๋ฆฌ (์ž๋™์œผ๋กœ CSRF ๊ฒ€์ฆ๋จ)
    @PostMapping("/api/submit")
    public ResponseEntity<String> submitData(@RequestBody DataDto data) {
        // CSRF ํ† ํฐ์ด ์œ ํšจํ•˜๋ฉด ์ด ๋ฉ”์„œ๋“œ๊ฐ€ ์‹คํ–‰๋จ
        return ResponseEntity.ok("๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ ์™„๋ฃŒ! ๐ŸŽ‰");
    }
}

Spring์˜ ์žฅ์ ์€ ํ”„๋ ˆ์ž„์›Œํฌ ๋ ˆ๋ฒจ์—์„œ CSRF ๋ณดํ˜ธ๋ฅผ ์ œ๊ณตํ•œ๋‹ค๋Š” ๊ฑฐ์•ผ. ๊ฐœ๋ฐœ์ž๊ฐ€ ๋ณ„๋„๋กœ ๊ตฌํ˜„ํ•  ํ•„์š” ์—†์ด ์„ค์ •๋งŒ ํ•ด์ฃผ๋ฉด ๋! ๊ทผ๋ฐ SPA(Single Page Application)๋‚˜ REST API๋ฅผ ๋งŒ๋“ค ๋•Œ๋Š” ์„ค์ •์„ ์ข€ ๋” ์„ธ๋ฐ€ํ•˜๊ฒŒ ์กฐ์ •ํ•ด์•ผ ํ•ด.

โš›๏ธ React + REST API์—์„œ CSRF ํ† ํฐ ์ฒ˜๋ฆฌ

์š”์ฆ˜ ๋Œ€์„ธ์ธ SPA ํ™˜๊ฒฝ์—์„œ๋Š” CSRF ํ† ํฐ ์ฒ˜๋ฆฌ๊ฐ€ ์ข€ ๋‹ฌ๋ผ ใ…‹ใ…‹ ์ „ํ†ต์ ์ธ ํผ ์ œ์ถœ ๋ฐฉ์‹์ด ์•„๋‹ˆ๋ผ AJAX/Fetch๋กœ ํ†ต์‹ ํ•˜๋‹ˆ๊นŒ ๋ง์ด์•ผ.

์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ๋„ ํ”„๋ก ํŠธ์—”๋“œ ๊ฐœ๋ฐœ ์žฌ๋Šฅ์„ ๊ฑฐ๋ž˜ํ•  ๋•Œ ์ด๋Ÿฐ ๋ณด์•ˆ ์ง€์‹์ด ์žˆ์œผ๋ฉด ์ง„์งœ ์œ ๋ฆฌํ•˜๊ฑฐ๋“ ? ํด๋ผ์ด์–ธํŠธ ์ธก ๋ณด์•ˆ ๊ตฌํ˜„ ๋Šฅ๋ ฅ์€ ํ•„์ˆ˜์•ผ ใ…‡ใ…‡

โš›๏ธ React์—์„œ CSRF ํ† ํฐ ๊ด€๋ฆฌํ•˜๊ธฐ
// csrfToken.js - CSRF ํ† ํฐ ์œ ํ‹ธ๋ฆฌํ‹ฐ
export const getCsrfToken = async () => {
    try {
        const response = await fetch('/api/csrf-token', {
            credentials: 'include' // ์ฟ ํ‚ค ํฌํ•จ
        });
        const data = await response.json();
        return data.csrfToken;
    } catch (error) {
        console.error('CSRF ํ† ํฐ ๊ฐ€์ ธ์˜ค๊ธฐ ์‹คํŒจ ใ… ใ… ', error);
        return null;
    }
};

// ์ฟ ํ‚ค์—์„œ CSRF ํ† ํฐ ์ฝ๊ธฐ
export const getCsrfTokenFromCookie = () => {
    const name = 'XSRF-TOKEN=';
    const decodedCookie = decodeURIComponent(document.cookie);
    const cookies = decodedCookie.split(';');
    
    for (let cookie of cookies) {
        cookie = cookie.trim();
        if (cookie.indexOf(name) === 0) {
            return cookie.substring(name.length);
        }
    }
    return null;
};

// API ํ˜ธ์ถœ ๋ž˜ํผ ํ•จ์ˆ˜
export const apiCall = async (url, options = {}) => {
    const token = getCsrfTokenFromCookie() || await getCsrfToken();
    
    const defaultOptions = {
        credentials: 'include',
        headers: {
            'Content-Type': 'application/json',
            'X-CSRF-Token': token,
            ...options.headers
        }
    };
    
    return fetch(url, { ...defaultOptions, ...options });
};
๐ŸŽจ React ์ปดํฌ๋„ŒํŠธ์—์„œ ์‚ฌ์šฉํ•˜๊ธฐ
import React, { useState, useEffect } from 'react';
import { apiCall, getCsrfToken } from './csrfToken';

function ProtectedForm() {
    const [csrfToken, setCsrfToken] = useState('');
    const [formData, setFormData] = useState({ name: '', email: '' });
    const [message, setMessage] = useState('');

    // ์ปดํฌ๋„ŒํŠธ ๋งˆ์šดํŠธ ์‹œ CSRF ํ† ํฐ ๊ฐ€์ ธ์˜ค๊ธฐ
    useEffect(() => {
        const fetchToken = async () => {
            const token = await getCsrfToken();
            setCsrfToken(token);
        };
        fetchToken();
    }, []);

    const handleSubmit = async (e) => {
        e.preventDefault();
        
        try {
            const response = await apiCall('/api/submit', {
                method: 'POST',
                body: JSON.stringify(formData)
            });
            
            if (response.ok) {
                const result = await response.json();
                setMessage('์ „์†ก ์„ฑ๊ณต! ๐ŸŽ‰');
            } else {
                setMessage('์ „์†ก ์‹คํŒจ ๐Ÿ˜ข');
            }
        } catch (error) {
            console.error('์—๋Ÿฌ ๋ฐœ์ƒ:', error);
            setMessage('๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜ ๋ฐœ์ƒ ใ… ใ… ');
        }
    };

    return (
        <div>
            <form onSubmit={handleSubmit}>
                <input
                    type="text"
                    value={formData.name}
                    onChange={(e) => setFormData({...formData, name: e.target.value})}
                    placeholder="์ด๋ฆ„"
                />
                <input
                    type="email"
                    value={formData.email}
                    onChange={(e) => setFormData({...formData, email: e.target.value})}
                    placeholder="์ด๋ฉ”์ผ"
                />
                <button type="submit">์ „์†ก</button>
            </form>
            {message && <p>{message}</p>}
        </div>
    );
}

export default ProtectedForm;
๐Ÿ”„ Axios ์ธํ„ฐ์…‰ํ„ฐ๋กœ ์ž๋™ํ™”ํ•˜๊ธฐ
import axios from 'axios';

// Axios ์ธ์Šคํ„ด์Šค ์ƒ์„ฑ
const api = axios.create({
    baseURL: '/api',
    withCredentials: true // ์ฟ ํ‚ค ์ž๋™ ํฌํ•จ
});

// ์š”์ฒญ ์ธํ„ฐ์…‰ํ„ฐ: ๋ชจ๋“  ์š”์ฒญ์— CSRF ํ† ํฐ ์ž๋™ ์ถ”๊ฐ€
api.interceptors.request.use(
    async (config) => {
        // ์ฟ ํ‚ค์—์„œ ํ† ํฐ ์ฝ๊ธฐ
        const token = document.cookie
            .split('; ')
            .find(row => row.startsWith('XSRF-TOKEN='))
            ?.split('=')[1];
        
        if (token) {
            config.headers['X-CSRF-Token'] = token;
        }
        
        return config;
    },
    (error) => {
        return Promise.reject(error);
    }
);

// ์‘๋‹ต ์ธํ„ฐ์…‰ํ„ฐ: CSRF ์—๋Ÿฌ ์ฒ˜๋ฆฌ
api.interceptors.response.use(
    (response) => response,
    async (error) => {
        if (error.response?.status === 403) {
            console.error('CSRF ํ† ํฐ ๊ฒ€์ฆ ์‹คํŒจ! ํ† ํฐ์„ ๊ฐฑ์‹ ํ•ด์•ผ ํ•จ ใ… ใ… ');
            // ํ† ํฐ ๊ฐฑ์‹  ๋กœ์ง ์ถ”๊ฐ€ ๊ฐ€๋Šฅ
        }
        return Promise.reject(error);
    }
);

// ์‚ฌ์šฉ ์˜ˆ์‹œ
export const submitData = async (data) => {
    try {
        const response = await api.post('/submit', data);
        return response.data;
    } catch (error) {
        console.error('API ํ˜ธ์ถœ ์‹คํŒจ:', error);
        throw error;
    }
};

export default api;

Axios ์ธํ„ฐ์…‰ํ„ฐ๋ฅผ ์“ฐ๋ฉด ์ง„์งœ ํŽธํ•ด ใ…‹ใ…‹ใ…‹ ๋ชจ๋“  ์š”์ฒญ์— ์ž๋™์œผ๋กœ CSRF ํ† ํฐ์ด ๋ถ™์œผ๋‹ˆ๊นŒ ์ผ์ผ์ด ์‹ ๊ฒฝ ์“ธ ํ•„์š”๊ฐ€ ์—†๊ฑฐ๋“ . ์ด๋Ÿฐ ํŒจํ„ด์€ ๋Œ€๊ทœ๋ชจ ํ”„๋กœ์ ํŠธ์—์„œ ํ•„์ˆ˜์•ผ!

๐Ÿ”’ Double Submit Cookie ํŒจํ„ด

์„ธ์…˜์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” stateless API์—์„œ๋Š” ์–ด๋–ป๊ฒŒ CSRF๋ฅผ ๋ง‰์„๊นŒ? ์—ฌ๊ธฐ์„œ ๋“ฑ์žฅํ•˜๋Š” ๊ฒŒ Double Submit Cookie ํŒจํ„ด์ด์•ผ ใ…‹ใ…‹

์ด ๋ฐฉ์‹์€ ์„œ๋ฒ„๊ฐ€ ํ† ํฐ์„ ์ €์žฅํ•˜์ง€ ์•Š๊ณ ๋„ CSRF๋ฅผ ๋ฐฉ์–ดํ•  ์ˆ˜ ์žˆ์–ด. ์›๋ฆฌ๋Š” ๊ฐ„๋‹จํ•ด: ๊ฐ™์€ ํ† ํฐ์„ ์ฟ ํ‚ค์™€ ์š”์ฒญ ํ—ค๋”(๋˜๋Š” ๋ฐ”๋””) ์–‘์ชฝ์— ๋ณด๋‚ด๋Š” ๊ฑฐ์ง€. ๊ณต๊ฒฉ์ž๋Š” ์ฟ ํ‚ค๋ฅผ ์ฝ์„ ์ˆ˜ ์—†์œผ๋‹ˆ๊นŒ ํ—ค๋”์— ์˜ฌ๋ฐ”๋ฅธ ํ† ํฐ์„ ๋„ฃ์„ ์ˆ˜ ์—†๋‹ค๋Š” ๊ฒŒ ํ•ต์‹ฌ!

๐Ÿช Double Submit Cookie ๊ตฌํ˜„ (Node.js)
const express = require('express');
const cookieParser = require('cookie-parser');
const crypto = require('crypto');

const app = express();
app.use(cookieParser());
app.use(express.json());

// CSRF ํ† ํฐ ์ƒ์„ฑ ๋ฐ ์ฟ ํ‚ค ์„ค์ •
app.use((req, res, next) => {
    if (!req.cookies.csrfToken) {
        const token = crypto.randomBytes(32).toString('hex');
        res.cookie('csrfToken', token, {
            httpOnly: false, // JavaScript์—์„œ ์ฝ์„ ์ˆ˜ ์žˆ์–ด์•ผ ํ•จ!
            secure: true,
            sameSite: 'strict',
            maxAge: 3600000 // 1์‹œ๊ฐ„
        });
    }
    next();
});

// CSRF ๊ฒ€์ฆ ๋ฏธ๋“ค์›จ์–ด
function verifyDoubleSubmitToken(req, res, next) {
    const cookieToken = req.cookies.csrfToken;
    const headerToken = req.headers['x-csrf-token'];
    
    // ์ฟ ํ‚ค์™€ ํ—ค๋”์˜ ํ† ํฐ์ด ์ผ์น˜ํ•˜๋Š”์ง€ ํ™•์ธ
    if (!cookieToken || !headerToken || cookieToken !== headerToken) {
        return res.status(403).json({ 
            error: 'CSRF ํ† ํฐ ๋ถˆ์ผ์น˜! ๊ณต๊ฒฉ ์‹œ๋„ ๊ฐ์ง€ ๐Ÿšจ' 
        });
    }
    
    next();
}

// ํ† ํฐ ๋ฐœ๊ธ‰ ์—”๋“œํฌ์ธํŠธ
app.get('/api/token', (req, res) => {
    res.json({ 
        csrfToken: req.cookies.csrfToken,
        message: 'ํ† ํฐ ๋ฐœ๊ธ‰ ์™„๋ฃŒ โœ…'
    });
});

// ๋ณดํ˜ธ๋œ API ์—”๋“œํฌ์ธํŠธ
app.post('/api/protected', verifyDoubleSubmitToken, (req, res) => {
    res.json({ 
        success: true, 
        message: '์š”์ฒญ์ด ์•ˆ์ „ํ•˜๊ฒŒ ์ฒ˜๋ฆฌ๋˜์—ˆ์Šต๋‹ˆ๋‹ค! ๐ŸŽ‰',
        data: req.body 
    });
});

app.listen(3000);
๐ŸŒ ํด๋ผ์ด์–ธํŠธ ์ธก ๊ตฌํ˜„
// ์ฟ ํ‚ค์—์„œ CSRF ํ† ํฐ ์ฝ๊ธฐ
function getCookie(name) {
    const value = `; ${document.cookie}`;
    const parts = value.split(`; ${name}=`);
    if (parts.length === 2) {
        return parts.pop().split(';').shift();
    }
    return null;
}

// API ํ˜ธ์ถœ ํ•จ์ˆ˜
async function makeProtectedRequest(data) {
    const csrfToken = getCookie('csrfToken');
    
    if (!csrfToken) {
        console.error('CSRF ํ† ํฐ์ด ์—†์Œ ใ… ใ… ');
        return;
    }
    
    try {
        const response = await fetch('/api/protected', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'X-CSRF-Token': csrfToken // ์ฟ ํ‚ค ๊ฐ’์„ ํ—ค๋”์—๋„ ์ „์†ก
            },
            credentials: 'include', // ์ฟ ํ‚ค ํฌํ•จ
            body: JSON.stringify(data)
        });
        
        const result = await response.json();
        console.log('์‘๋‹ต:', result);
        return result;
    } catch (error) {
        console.error('์š”์ฒญ ์‹คํŒจ:', error);
    }
}

// ์‚ฌ์šฉ ์˜ˆ์‹œ
makeProtectedRequest({ 
    username: 'testuser', 
    action: 'update_profile' 
});
๐Ÿ’ก Double Submit Cookie์˜ ์žฅ๋‹จ์  ์žฅ์ :
โœ… ์„œ๋ฒ„์—์„œ ํ† ํฐ์„ ์ €์žฅํ•  ํ•„์š” ์—†์Œ (stateless)
โœ… ์„ธ์…˜ ๊ด€๋ฆฌ ๋ถˆํ•„์š”
โœ… ํ™•์žฅ์„ฑ์ด ์ข‹์Œ (์—ฌ๋Ÿฌ ์„œ๋ฒ„์—์„œ ๋™์ž‘ ๊ฐ€๋Šฅ)
โœ… ๊ตฌํ˜„์ด ๋น„๊ต์  ๊ฐ„๋‹จ

๋‹จ์ :
โŒ ์„œ๋ธŒ๋„๋ฉ”์ธ ๊ณต๊ฒฉ์— ์ทจ์•ฝํ•  ์ˆ˜ ์žˆ์Œ
โŒ httpOnly ์ฟ ํ‚ค๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์Œ
โŒ XSS ๊ณต๊ฒฉ์— ๋…ธ์ถœ๋˜๋ฉด ํ† ํฐ์ด ํƒˆ์ทจ๋  ์ˆ˜ ์žˆ์Œ

๐ŸŽญ HMAC ๊ธฐ๋ฐ˜ CSRF ํ† ํฐ

์ข€ ๋” ๊ณ ๊ธ‰ ๊ธฐ๋ฒ•์œผ๋กœ ๋„˜์–ด๊ฐ€๋ณผ๊นŒ? ใ…‹ใ…‹ HMAC(Hash-based Message Authentication Code)๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ํ† ํฐ์˜ ๋ฌด๊ฒฐ์„ฑ์„ ๋ณด์žฅํ•  ์ˆ˜ ์žˆ์–ด. ์„œ๋ฒ„๋งŒ ์•Œ๊ณ  ์žˆ๋Š” ๋น„๋ฐ€ ํ‚ค๋กœ ํ† ํฐ์„ ์„œ๋ช…ํ•˜๋Š” ๋ฐฉ์‹์ด์ง€.

๐Ÿ” HMAC ๊ธฐ๋ฐ˜ ํ† ํฐ ์ƒ์„ฑ (Node.js)
const crypto = require('crypto');

class CSRFTokenManager {
    constructor(secretKey) {
        this.secretKey = secretKey;
    }
    
    // CSRF ํ† ํฐ ์ƒ์„ฑ
    generateToken(sessionId, timestamp = Date.now()) {
        // ๋žœ๋ค ๊ฐ’ ์ƒ์„ฑ
        const randomValue = crypto.randomBytes(16).toString('hex');
        
        // ํ† ํฐ ํŽ˜์ด๋กœ๋“œ: sessionId + timestamp + randomValue
        const payload = `${sessionId}:${timestamp}:${randomValue}`;
        
        // HMAC ์„œ๋ช… ์ƒ์„ฑ
        const signature = crypto
            .createHmac('sha256', this.secretKey)
            .update(payload)
            .digest('hex');
        
        // ์ตœ์ข… ํ† ํฐ: payload + signature
        const token = `${payload}:${signature}`;
        
        return Buffer.from(token).toString('base64');
    }
    
    // CSRF ํ† ํฐ ๊ฒ€์ฆ
    verifyToken(token, sessionId, maxAge = 3600000) {
        try {
            // Base64 ๋””์ฝ”๋”ฉ
            const decoded = Buffer.from(token, 'base64').toString('utf-8');
            const parts = decoded.split(':');
            
            if (parts.length !== 4) {
                return { valid: false, reason: 'ํ† ํฐ ํ˜•์‹ ์˜ค๋ฅ˜' };
            }
            
            const [tokenSessionId, timestamp, randomValue, signature] = parts;
            
            // ์„ธ์…˜ ID ํ™•์ธ
            if (tokenSessionId !== sessionId) {
                return { valid: false, reason: '์„ธ์…˜ ๋ถˆ์ผ์น˜' };
            }
            
            // ํƒ€์ž„์Šคํƒฌํ”„ ํ™•์ธ (๋งŒ๋ฃŒ ์ฒดํฌ)
            const tokenAge = Date.now() - parseInt(timestamp);
            if (tokenAge > maxAge) {
                return { valid: false, reason: 'ํ† ํฐ ๋งŒ๋ฃŒ๋จ โฐ' };
            }
            
            // ์„œ๋ช… ๊ฒ€์ฆ
            const payload = `${tokenSessionId}:${timestamp}:${randomValue}`;
            const expectedSignature = crypto
                .createHmac('sha256', this.secretKey)
                .update(payload)
                .digest('hex');
            
            if (signature !== expectedSignature) {
                return { valid: false, reason: '์„œ๋ช… ๊ฒ€์ฆ ์‹คํŒจ ๐Ÿšซ' };
            }
            
            return { valid: true, reason: 'ํ† ํฐ ์œ ํšจ โœ…' };
            
        } catch (error) {
            return { valid: false, reason: 'ํ† ํฐ ํŒŒ์‹ฑ ์˜ค๋ฅ˜' };
        }
    }
}

// ์‚ฌ์šฉ ์˜ˆ์‹œ
const secretKey = crypto.randomBytes(32).toString('hex');
const csrfManager = new CSRFTokenManager(secretKey);

// ํ† ํฐ ์ƒ์„ฑ
const sessionId = 'user-session-12345';
const token = csrfManager.generateToken(sessionId);
console.log('์ƒ์„ฑ๋œ ํ† ํฐ:', token);

// ํ† ํฐ ๊ฒ€์ฆ
const verification = csrfManager.verifyToken(token, sessionId);
console.log('๊ฒ€์ฆ ๊ฒฐ๊ณผ:', verification);
// ์ถœ๋ ฅ: { valid: true, reason: 'ํ† ํฐ ์œ ํšจ โœ…' }
๐Ÿš€ Express ๋ฏธ๋“ค์›จ์–ด๋กœ ํ†ตํ•ฉ
const express = require('express');
const session = require('express-session');

const app = express();

// ์‹œํฌ๋ฆฟ ํ‚ค๋Š” ํ™˜๊ฒฝ๋ณ€์ˆ˜์—์„œ ๊ฐ€์ ธ์˜ค๊ธฐ
const SECRET_KEY = process.env.CSRF_SECRET || crypto.randomBytes(32).toString('hex');
const csrfManager = new CSRFTokenManager(SECRET_KEY);

app.use(session({
    secret: 'session-secret',
    resave: false,
    saveUninitialized: true
}));

// CSRF ํ† ํฐ ์ƒ์„ฑ ๋ฏธ๋“ค์›จ์–ด
app.use((req, res, next) => {
    if (req.session && req.session.id) {
        req.csrfToken = () => csrfManager.generateToken(req.session.id);
        res.locals.csrfToken = req.csrfToken();
    }
    next();
});

// CSRF ๊ฒ€์ฆ ๋ฏธ๋“ค์›จ์–ด
function validateCSRF(req, res, next) {
    const token = req.body._csrf || req.headers['x-csrf-token'];
    
    if (!token) {
        return res.status(403).json({ error: 'CSRF ํ† ํฐ ๋ˆ„๋ฝ' });
    }
    
    const result = csrfManager.verifyToken(token, req.session.id);
    
    if (!result.valid) {
        return res.status(403).json({ 
            error: 'CSRF ๊ฒ€์ฆ ์‹คํŒจ', 
            reason: result.reason 
        });
    }
    
    next();
}

// ๋ผ์šฐํŠธ ์˜ˆ์‹œ
app.get('/form', (req, res) => {
    res.send(`
        <form method="POST" action="/submit">
            <input type="hidden" name="_csrf" value="${res.locals.csrfToken}">
            <input type="text" name="data">
            <button>์ „์†ก</button>
        </form>
    `);
});

app.post('/submit', validateCSRF, (req, res) => {
    res.json({ success: true, message: '์•ˆ์ „ํ•˜๊ฒŒ ์ฒ˜๋ฆฌ๋จ! ๐ŸŽ‰' });
});

app.listen(3000);

HMAC ๋ฐฉ์‹์˜ ์žฅ์ ์€ ํ† ํฐ ์ž์ฒด์— ๊ฒ€์ฆ ์ •๋ณด๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ๋‹ค๋Š” ๊ฑฐ์•ผ. ์„œ๋ฒ„๊ฐ€ ๋ณ„๋„๋กœ ํ† ํฐ์„ ์ €์žฅํ•˜์ง€ ์•Š์•„๋„ ๋˜๊ณ , ํ† ํฐ์ด ์œ„์กฐ๋˜์—ˆ๋Š”์ง€ ์ฆ‰์‹œ ์•Œ ์ˆ˜ ์žˆ์ง€ ใ…‡ใ…‡ ๋ณด์•ˆ์„ฑ๋„ ํ›จ์”ฌ ๋†’๊ณ !

๐Ÿ“ฑ ๋ชจ๋ฐ”์ผ ์•ฑ์—์„œ์˜ CSRF ๋ฐฉ์–ด

๋ชจ๋ฐ”์ผ ์•ฑ์€ ์›น๊ณผ ์ข€ ๋‹ค๋ฅด๊ฒŒ ์ ‘๊ทผํ•ด์•ผ ํ•ด ใ…‹ใ…‹ ๋„ค์ดํ‹ฐ๋ธŒ ์•ฑ์€ ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ์•„๋‹ˆ๋‹ˆ๊นŒ ์ „ํ†ต์ ์ธ CSRF ๊ณต๊ฒฉ์—๋Š” ๋œ ์ทจ์•ฝํ•˜์ง€๋งŒ, ๊ทธ๋ž˜๋„ ๋ฐฉ์–ด ๋ฉ”์ปค๋‹ˆ์ฆ˜์€ ํ•„์š”ํ•ด!

๐Ÿ“ฑ React Native์—์„œ CSRF ํ† ํฐ ์ฒ˜๋ฆฌ
// csrfService.js
import AsyncStorage from '@react-native-async-storage/async-storage';

class CSRFService {
    constructor() {
        this.tokenKey = '@csrf_token';
        this.baseURL = 'https://api.example.com';
    }
    
    // ํ† ํฐ ๊ฐ€์ ธ์˜ค๊ธฐ
    async getToken() {
        try {
            const token = await AsyncStorage.getItem(this.tokenKey);
            if (token) {
                return token;
            }
            // ํ† ํฐ์ด ์—†์œผ๋ฉด ์„œ๋ฒ„์—์„œ ์ƒˆ๋กœ ๋ฐ›์•„์˜ค๊ธฐ
            return await this.fetchNewToken();
        } catch (error) {
            console.error('ํ† ํฐ ๊ฐ€์ ธ์˜ค๊ธฐ ์‹คํŒจ ใ… ใ… ', error);
            return null;
        }
    }
    
    // ์„œ๋ฒ„์—์„œ ์ƒˆ ํ† ํฐ ๋ฐ›์•„์˜ค๊ธฐ
    async fetchNewToken() {
        try {
            const response = await fetch(`${this.baseURL}/api/csrf-token`, {
                method: 'GET',
                credentials: 'include'
            });
            
            const data = await response.json();
            await AsyncStorage.setItem(this.tokenKey, data.token);
            return data.token;
        } catch (error) {
            console.error('ํ† ํฐ ๋ฐœ๊ธ‰ ์‹คํŒจ:', error);
            return null;
        }
    }
    
    // ํ† ํฐ ๊ฐฑ์‹ 
    async refreshToken() {
        await AsyncStorage.removeItem(this.tokenKey);
        return await this.fetchNewToken();
    }
    
    // API ํ˜ธ์ถœ ๋ž˜ํผ
    async apiCall(endpoint, options = {}) {
        const token = await this.getToken();
        
        const defaultOptions = {
            headers: {
                'Content-Type': 'application/json',
                'X-CSRF-Token': token,
                ...options.headers
            }
        };
        
        try {
            const response = await fetch(
                `${this.baseURL}${endpoint}`, 
                { ...defaultOptions, ...options }
            );
            
            // 403 ์—๋Ÿฌ๋ฉด ํ† ํฐ ๊ฐฑ์‹  ํ›„ ์žฌ์‹œ๋„
            if (response.status === 403) {
                console.log('ํ† ํฐ ๋งŒ๋ฃŒ, ๊ฐฑ์‹  ์ค‘...');
                await this.refreshToken();
                
                // ์žฌ์‹œ๋„
                const newToken = await this.getToken();
                defaultOptions.headers['X-CSRF-Token'] = newToken;
                return await fetch(
                    `${this.baseURL}${endpoint}`, 
                    { ...defaultOptions, ...options }
                );
            }
            
            return response;
        } catch (error) {
            console.error('API ํ˜ธ์ถœ ์‹คํŒจ:', error);
            throw error;
        }
    }
}

export default new CSRFService();
๐ŸŽจ React Native ์ปดํฌ๋„ŒํŠธ์—์„œ ์‚ฌ์šฉ
import React, { useState } from 'react';
import { View, TextInput, Button, Text, Alert } from 'react-native';
import csrfService from './csrfService';

function ProtectedForm() {
    const [name, setName] = useState('');
    const [email, setEmail] = useState('');
    const [loading, setLoading] = useState(false);
    
    const handleSubmit = async () => {
        setLoading(true);
        
        try {
            const response = await csrfService.apiCall('/api/submit', {
                method: 'POST',
                body: JSON.stringify({ name, email })
            });
            
            const result = await response.json();
            
            if (response.ok) {
                Alert.alert('์„ฑ๊ณต', '๋ฐ์ดํ„ฐ๊ฐ€ ์ „์†ก๋˜์—ˆ์Šต๋‹ˆ๋‹ค! ๐ŸŽ‰');
                setName('');
                setEmail('');
            } else {
                Alert.alert('์˜ค๋ฅ˜', result.error || '์ „์†ก ์‹คํŒจ ใ… ใ… ');
            }
        } catch (error) {
            Alert.alert('์˜ค๋ฅ˜', '๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค');
        } finally {
            setLoading(false);
        }
    };
    
    return (
        <View style={{ padding: 20 }}>
            <TextInput
                placeholder="์ด๋ฆ„"
                value={name}
                onChangeText={setName}
                style={{ borderWidth: 1, padding: 10, marginBottom: 10 }}
            />
            <TextInput
                placeholder="์ด๋ฉ”์ผ"
                value={email}
                onChangeText={setEmail}
                keyboardType="email-address"
                style={{ borderWidth: 1, padding: 10, marginBottom: 10 }}
            />
            <Button 
                title={loading ? "์ „์†ก ์ค‘..." : "์ „์†ก"} 
                onPress={handleSubmit}
                disabled={loading}
            />
        </View>
    );
}

export default ProtectedForm;

๋ชจ๋ฐ”์ผ ์•ฑ์—์„œ๋Š” ํ† ํฐ์„ AsyncStorage๋‚˜ SecureStore์— ์ €์žฅํ•˜๊ณ , API ํ˜ธ์ถœ ์‹œ๋งˆ๋‹ค ์ž๋™์œผ๋กœ ํฌํ•จ์‹œํ‚ค๋Š” ๊ฒŒ ์ผ๋ฐ˜์ ์ด์•ผ. ํ† ํฐ ๋งŒ๋ฃŒ ์‹œ ์ž๋™ ๊ฐฑ์‹  ๋กœ์ง๋„ ๊ผญ ๋„ฃ์–ด์ค˜์•ผ ํ•˜๊ณ !

๐Ÿ”„ ํ† ํฐ ๊ฐฑ์‹  ์ „๋žต

CSRF ํ† ํฐ๋„ ๋งŒ๋ฃŒ ์‹œ๊ฐ„์ด ์žˆ์–ด์•ผ ํ•ด. ๋„ˆ๋ฌด ์˜ค๋ž˜ ์œ ํšจํ•˜๋ฉด ๋ณด์•ˆ ์œ„ํ—˜์ด ์ปค์ง€๊ฑฐ๋“  ใ…‹ใ…‹ ๊ทผ๋ฐ ์‚ฌ์šฉ์ž๊ฐ€ ํผ ์ž‘์„ฑ ์ค‘์— ํ† ํฐ์ด ๋งŒ๋ฃŒ๋˜๋ฉด? ์งœ์ฆ๋‚˜์ง€ ใ… ใ…  ๊ทธ๋ž˜์„œ ์ ์ ˆํ•œ ๊ฐฑ์‹  ์ „๋žต์ด ํ•„์š”ํ•ด!

๐Ÿ”„ ์ž๋™ ํ† ํฐ ๊ฐฑ์‹  ๊ตฌํ˜„
class TokenRefreshManager {
    constructor(refreshInterval = 1800000) { // ๊ธฐ๋ณธ 30๋ถ„
        this.refreshInterval = refreshInterval;
        this.refreshTimer = null;
        this.token = null;
    }
    
    // ํ† ํฐ ๊ฐฑ์‹  ์‹œ์ž‘
    startAutoRefresh() {
        // ์ดˆ๊ธฐ ํ† ํฐ ๊ฐ€์ ธ์˜ค๊ธฐ
        this.refreshToken();
        
        // ์ฃผ๊ธฐ์  ๊ฐฑ์‹  ์„ค์ •
        this.refreshTimer = setInterval(() => {
            this.refreshToken();
        }, this.refreshInterval);
        
        console.log('์ž๋™ ํ† ํฐ ๊ฐฑ์‹  ์‹œ์ž‘ ๐Ÿ”„');
    }
    
    // ํ† ํฐ ๊ฐฑ์‹  ์ค‘์ง€
    stopAutoRefresh() {
        if (this.refreshTimer) {
            clearInterval(this.refreshTimer);
            this.refreshTimer = null;
            console.log('์ž๋™ ํ† ํฐ ๊ฐฑ์‹  ์ค‘์ง€ โธ๏ธ');
        }
    }
    
    // ํ† ํฐ ๊ฐฑ์‹ 
    async refreshToken() {
        try {
            const response = await fetch('/api/csrf-token/refresh', {
                method: 'POST',
                credentials: 'include'
            });
            
            if (response.ok) {
                const data = await response.json();
                this.token = data.token;
                
                // ์ปค์Šคํ…€ ์ด๋ฒคํŠธ ๋ฐœ์ƒ (๋‹ค๋ฅธ ์ปดํฌ๋„ŒํŠธ์—์„œ ๊ฐ์ง€ ๊ฐ€๋Šฅ)
                window.dispatchEvent(new CustomEvent('csrf-token-refreshed', {
                    detail: { token: this.token }
                }));
                
                console.log('ํ† ํฐ ๊ฐฑ์‹  ์™„๋ฃŒ โœ…');
                return this.token;
            } else {
                console.error('ํ† ํฐ ๊ฐฑ์‹  ์‹คํŒจ ใ… ใ… ');
                return null;
            }
        } catch (error) {
            console.error('ํ† ํฐ ๊ฐฑ์‹  ์ค‘ ์˜ค๋ฅ˜:', error);
            return null;
        }
    }
    
    // ํ˜„์žฌ ํ† ํฐ ๊ฐ€์ ธ์˜ค๊ธฐ
    getToken() {
        return this.token;
    }
}

// ์ „์—ญ ์ธ์Šคํ„ด์Šค ์ƒ์„ฑ
const tokenManager = new TokenRefreshManager();

// ํŽ˜์ด์ง€ ๋กœ๋“œ ์‹œ ์ž๋™ ๊ฐฑ์‹  ์‹œ์ž‘
window.addEventListener('load', () => {
    tokenManager.startAutoRefresh();
});

// ํŽ˜์ด์ง€ ์–ธ๋กœ๋“œ ์‹œ ๊ฐฑ์‹  ์ค‘์ง€
window.addEventListener('beforeunload', () => {
    tokenManager.stopAutoRefresh();
});

export default tokenManager;
โšก React Hook์œผ๋กœ ํ† ํฐ ๊ด€๋ฆฌ
import { useState, useEffect, useCallback } from 'react';

function useCSRFToken(refreshInterval = 1800000) {
    const [token, setToken] = useState(null);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState(null);
    
    // ํ† ํฐ ๊ฐ€์ ธ์˜ค๊ธฐ
    const fetchToken = useCallback(async () => {
        try {
            setLoading(true);
            const response = await fetch('/api/csrf-token');
            
            if (!response.ok) {
                throw new Error('ํ† ํฐ ๊ฐ€์ ธ์˜ค๊ธฐ ์‹คํŒจ');
            }
            
            const data = await response.json();
            setToken(data.token);
            setError(null);
        } catch (err) {
            setError(err.message);
            console.error('ํ† ํฐ ๋กœ๋“œ ์˜ค๋ฅ˜:', err);
        } finally {
            setLoading(false);
        }
    }, []);
    
    // ํ† ํฐ ๊ฐฑ์‹ 
    const refreshToken = useCallback(async () => {
        console.log('ํ† ํฐ ๊ฐฑ์‹  ์ค‘... ๐Ÿ”„');
        await fetchToken();
    }, [fetchToken]);
    
    // ์ดˆ๊ธฐ ๋กœ๋“œ ๋ฐ ์ž๋™ ๊ฐฑ์‹  ์„ค์ •
    useEffect(() => {
        fetchToken();
        
        // ์ฃผ๊ธฐ์  ๊ฐฑ์‹ 
        const interval = setInterval(refreshToken, refreshInterval);
        
        return () => {
            clearInterval(interval);
        };
    }, [fetchToken, refreshToken, refreshInterval]);
    
    return { token, loading, error, refreshToken };
}

// ์‚ฌ์šฉ ์˜ˆ์‹œ
function MyComponent() {
    const { token, loading, error, refreshToken } = useCSRFToken();
    
    if (loading) return <div>ํ† ํฐ ๋กœ๋”ฉ ์ค‘... โณ</div>;
    if (error) return <div>์˜ค๋ฅ˜: {error} ๐Ÿ˜ข</div>;
    
    const handleSubmit = async (data) => {
        try {
            const response = await fetch('/api/submit', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'X-CSRF-Token': token
                },
                body: JSON.stringify(data)
            });
            
            if (response.status === 403) {
                // ํ† ํฐ ๋งŒ๋ฃŒ ์‹œ ๊ฐฑ์‹  ํ›„ ์žฌ์‹œ๋„
                await refreshToken();
                // ์žฌ์‹œ๋„ ๋กœ์ง...
            }
        } catch (error) {
            console.error('์ „์†ก ์‹คํŒจ:', error);
        }
    };
    
    return (
        <div>
            <p>ํ˜„์žฌ ํ† ํฐ: {token.substring(0, 20)}...</p>
            <button onClick={() => handleSubmit({ test: 'data' })}>
                ์ „์†ก
            </button>
        </div>
    );
}
๐ŸŽฏ ํ† ํฐ ๊ฐฑ์‹  ๋ฒ ์ŠคํŠธ ํ”„๋ž™ํ‹ฐ์Šค 1. ์ ์ ˆํ•œ ๊ฐฑ์‹  ์ฃผ๊ธฐ: ๋ณดํ†ต ํ† ํฐ ๋งŒ๋ฃŒ ์‹œ๊ฐ„์˜ 50-75% ์‹œ์ ์— ๊ฐฑ์‹ 
2. ๋ฐฑ๊ทธ๋ผ์šด๋“œ ๊ฐฑ์‹ : ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ๋ฐฉํ•ดํ•˜์ง€ ์•Š๋„๋ก ์กฐ์šฉํžˆ ๊ฐฑ์‹ 
3. ์‹คํŒจ ์ฒ˜๋ฆฌ: ๊ฐฑ์‹  ์‹คํŒจ ์‹œ ์žฌ์‹œ๋„ ๋กœ์ง ๊ตฌํ˜„
4. ์‚ฌ์šฉ์ž ํ™œ๋™ ๊ฐ์ง€: ์‚ฌ์šฉ์ž๊ฐ€ ํ™œ๋™ ์ค‘์ผ ๋•Œ๋งŒ ๊ฐฑ์‹  (๋ฆฌ์†Œ์Šค ์ ˆ์•ฝ)
5. ๋งŒ๋ฃŒ ์ž„๋ฐ• ๊ฒฝ๊ณ : ํ† ํฐ์ด ๊ณง ๋งŒ๋ฃŒ๋  ๋•Œ ์‚ฌ์šฉ์ž์—๊ฒŒ ์•Œ๋ฆผ

๐Ÿ› ๏ธ ์‹ค์ „ ๋””๋ฒ„๊น… ํŒ

CSRF ํ† ํฐ ๊ตฌํ˜„ํ•˜๋‹ค ๋ณด๋ฉด ๋ฌธ์ œ๊ฐ€ ์ƒ๊ธธ ๋•Œ๊ฐ€ ๋งŽ์•„ ใ…‹ใ…‹ใ…‹ ๋‚ด๊ฐ€ ๊ฒช์—ˆ๋˜ ์‚ฝ์งˆ๋“ค์„ ๊ณต์œ ํ•ด๋ณผ๊ฒŒ!

โš ๏ธ ์ž์ฃผ ๋ฐœ์ƒํ•˜๋Š” ๋ฌธ์ œ๋“ค 1. ํ† ํฐ์ด ๊ณ„์† invalid ๋œฌ๋‹ค?
โ†’ ์ฟ ํ‚ค ์„ค์ • ํ™•์ธ! sameSite, secure ์†์„ฑ ์ฒดํฌ
โ†’ ๋„๋ฉ”์ธ์ด ๋‹ค๋ฅด๋ฉด CORS ์„ค์ • ํ•„์š”
โ†’ ์„ธ์…˜์ด ์ œ๋Œ€๋กœ ์œ ์ง€๋˜๋Š”์ง€ ํ™•์ธ

2. AJAX ์š”์ฒญ์—์„œ๋งŒ ์‹คํŒจํ•œ๋‹ค?
โ†’ credentials: 'include' ๋น ๋œจ๋ ธ์„ ๊ฐ€๋Šฅ์„ฑ ๋†’์Œ
โ†’ ํ—ค๋”์— ํ† ํฐ ์ œ๋Œ€๋กœ ๋„ฃ์—ˆ๋Š”์ง€ ํ™•์ธ
โ†’ preflight ์š”์ฒญ(OPTIONS) ์ฒ˜๋ฆฌ ํ™•์ธ

3. ๋กœ์ปฌ์—์„œ๋Š” ๋˜๋Š”๋ฐ ๋ฐฐํฌํ•˜๋ฉด ์•ˆ ๋œ๋‹ค?
โ†’ HTTPS ์„ค์ • ํ™•์ธ (secure ์ฟ ํ‚ค๋Š” HTTPS ํ•„์ˆ˜)
โ†’ ํ”„๋ก์‹œ ์„ค์ • ๋ฌธ์ œ์ผ ์ˆ˜ ์žˆ์Œ
โ†’ ํ™˜๊ฒฝ๋ณ€์ˆ˜ ์ œ๋Œ€๋กœ ์„ค์ •๋๋Š”์ง€ ํ™•์ธ
๐Ÿ” ๋””๋ฒ„๊น… ํ—ฌํผ ํ•จ์ˆ˜
// CSRF ํ† ํฐ ๋””๋ฒ„๊น… ์œ ํ‹ธ๋ฆฌํ‹ฐ
class CSRFDebugger {
    static logTokenInfo(token, sessionId) {
        console.group('๐Ÿ” CSRF ํ† ํฐ ์ •๋ณด');
        console.log('ํ† ํฐ:', token);
        console.log('ํ† ํฐ ๊ธธ์ด:', token?.length);
        console.log('์„ธ์…˜ ID:', sessionId);
        console.log('์ฟ ํ‚ค:', document.cookie);
        console.groupEnd();
    }
    
    static validateTokenFormat(token) {
        if (!token) {
            console.error('โŒ ํ† ํฐ์ด ์—†์Œ!');
            return false;
        }
        
        if (typeof token !== 'string') {
            console.error('โŒ ํ† ํฐ์ด ๋ฌธ์ž์—ด์ด ์•„๋‹˜!');
            return false;
        }
        
        if (token.length < 32) {
            console.warn('โš ๏ธ ํ† ํฐ์ด ๋„ˆ๋ฌด ์งง์Œ (๋ณด์•ˆ ์ทจ์•ฝ)');
            return false;
        }
        
        console.log('โœ… ํ† ํฐ ํ˜•์‹ ์œ ํšจ');
        return true;
    }
    
    static async testCSRFProtection(endpoint, token) {
        console.group('๐Ÿงช CSRF ๋ณดํ˜ธ ํ…Œ์ŠคํŠธ');
        
        // 1. ํ† ํฐ ์—†์ด ์š”์ฒญ
        console.log('ํ…Œ์ŠคํŠธ 1: ํ† ํฐ ์—†์ด ์š”์ฒญ');
        try {
            const res1 = await fetch(endpoint, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ test: 'data' })
            });
            console.log('๊ฒฐ๊ณผ:', res1.status, res1.status === 403 ? 'โœ… ์ฐจ๋‹จ๋จ' : 'โŒ ํ†ต๊ณผ๋จ');
        } catch (e) {
            console.error('์˜ค๋ฅ˜:', e);
        }
        
        // 2. ์ž˜๋ชป๋œ ํ† ํฐ์œผ๋กœ ์š”์ฒญ
        console.log('ํ…Œ์ŠคํŠธ 2: ์ž˜๋ชป๋œ ํ† ํฐ์œผ๋กœ ์š”์ฒญ');
        try {
            const res2 = await fetch(endpoint, {
                method: 'POST',
                headers: { 
                    'Content-Type': 'application/json',
                    'X-CSRF-Token': 'invalid-token-12345'
                },
                body: JSON.stringify({ test: 'data' })
            });
            console.log('๊ฒฐ๊ณผ:', res2.status, res2.status === 403 ? 'โœ… ์ฐจ๋‹จ๋จ' : 'โŒ ํ†ต๊ณผ๋จ');
        } catch (e) {
            console.error('์˜ค๋ฅ˜:', e);
        }
        
        // 3. ์˜ฌ๋ฐ”๋ฅธ ํ† ํฐ์œผ๋กœ ์š”์ฒญ
        console.log('ํ…Œ์ŠคํŠธ 3: ์˜ฌ๋ฐ”๋ฅธ ํ† ํฐ์œผ๋กœ ์š”์ฒญ');
        try {
            const res3 = await fetch(endpoint, {
                method: 'POST',
                headers: { 
                    'Content-Type': 'application/json',
                    'X-CSRF-Token': token
                },
                body: JSON.stringify({ test: 'data' })
            });
            console.log('๊ฒฐ๊ณผ:', res3.status, res3.status === 200 ? 'โœ… ํ†ต๊ณผ๋จ' : 'โŒ ์ฐจ๋‹จ๋จ');
        } catch (e) {
            console.error('์˜ค๋ฅ˜:', e);
        }
        
        console.groupEnd();
    }
    
    static monitorCSRFRequests() {
        // Fetch API ๋ž˜ํ•‘
        const originalFetch = window.fetch;
        window.fetch = async (...args) => {
            const [url, options] = args;
            
            if (options?.method && options.method !== 'GET') {
                const token = options.headers?.['X-CSRF-Token'] || 
                             options.headers?.['x-csrf-token'];
                
                console.log('๐ŸŒ CSRF ๋ณดํ˜ธ ์š”์ฒญ:', {
                    url,
                    method: options.method,
                    hasToken: !!token,
                    token: token?.substring(0, 20) + '...'
                });
            }
            
            return originalFetch(...args);
        };
        
        console.log('โœ… CSRF ์š”์ฒญ ๋ชจ๋‹ˆํ„ฐ๋ง ํ™œ์„ฑํ™”');
    }
}

// ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์—์„œ๋งŒ ์‚ฌ์šฉ
if (process.env.NODE_ENV === 'development') {
    window.CSRFDebugger = CSRFDebugger;
    
    // ์ž๋™ ๋ชจ๋‹ˆํ„ฐ๋ง ์‹œ์ž‘
    CSRFDebugger.monitorCSRFRequests();
}

๋””๋ฒ„๊น… ๋„๊ตฌ๋ฅผ ๋งŒ๋“ค์–ด๋‘๋ฉด ์ง„์งœ ํŽธํ•ด ใ…‹ใ…‹ใ…‹ ๋ฌธ์ œ ์ƒ๊ฒผ์„ ๋•Œ ๋ฐ”๋กœ ์›์ธ ํŒŒ์•…ํ•  ์ˆ˜ ์žˆ๊ฑฐ๋“ . ํŠนํžˆ ํ”„๋กœ๋•์…˜ ๋ฐฐํฌ ์ „์— ๊ผญ ํ…Œ์ŠคํŠธํ•ด๋ด์•ผ ํ•ด!

๐ŸŒ ๋‹ค์–‘ํ•œ ํ”„๋ ˆ์ž„์›Œํฌ๋ณ„ ๊ตฌํ˜„

์ง€๊ธˆ๊นŒ์ง€ ์ฃผ๋กœ Node.js๋ž‘ Python์„ ๋ดค๋Š”๋ฐ, ๋‹ค๋ฅธ ์–ธ์–ด๋“ค๋„ ๊ฐ„๋‹จํžˆ ์‚ดํŽด๋ณผ๊ฒŒ ใ…‡ใ…‡

๐Ÿฆ€ Rust (Actix-web)
use actix_web::{web, App, HttpRequest, HttpResponse, HttpServer};
use actix_session::{Session, SessionMiddleware, storage::CookieSessionStore};
use actix_web::cookie::Key;
use rand::{thread_rng, Rng};
use rand::distributions::Alphanumeric;

// CSRF ํ† ํฐ ์ƒ์„ฑ
fn generate_csrf_token() -> String {
    thread_rng()
        .sample_iter(&Alphanumeric)
        .take(32)
        .map(char::from)
        .collect()
}

// CSRF ํ† ํฐ ๊ฒ€์ฆ ๋ฏธ๋“ค์›จ์–ด
async fn verify_csrf(req: HttpRequest, session: Session) -> Result<(), HttpResponse> {
    let session_token = session.get::<String>("csrf_token")?
        .ok_or_else(|| HttpResponse::Forbidden().body("No CSRF token in session"))?;
    
    let request_token = req
        .headers()
        .get("X-CSRF-Token")
        .and_then(|h| h.to_str().ok())
        .ok_or_else(|| HttpResponse::Forbidden().body("No CSRF token in request"))?;
    
    if session_token != request_token {
        return Err(HttpResponse::Forbidden().body("CSRF token mismatch"));
    }
    
    Ok(())
}

// ๋ผ์šฐํŠธ ํ•ธ๋“ค๋Ÿฌ
async fn get_form(session: Session) -> HttpResponse {
    let token = generate_csrf_token();
    session.insert("csrf_token", token.clone()).unwrap();
    
    HttpResponse::Ok().body(format!(
        r#"<form method="POST" action="/submit">
            <input type="hidden" name="csrf_token" value="{}">
            <button>Submit</button>
        </form>"#,
        token
    ))
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    let secret_key = Key::generate();
    
    HttpServer::new(move || {
        App::new()
            .wrap(SessionMiddleware::new(
                CookieSessionStore::default(),
                secret_key.clone()
            ))
            .route("/form", web::get().to(get_form))
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}
๐Ÿ’Ž Ruby on Rails
# Rails๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ CSRF ๋ณดํ˜ธ๊ฐ€ ํ™œ์„ฑํ™”๋˜์–ด ์žˆ์Œ
# app/controllers/application_controller.rb

class ApplicationController < ActionController::Base
  # CSRF ๋ณดํ˜ธ ํ™œ์„ฑํ™” (๊ธฐ๋ณธ๊ฐ’)
  protect_from_forgery with: :exception
  
  # ํŠน์ • ์•ก์…˜์—์„œ CSRF ๋ณดํ˜ธ ๊ฑด๋„ˆ๋›ฐ๊ธฐ
  skip_before_action :verify_authenticity_token, only: [:webhook]
  
  # API ์ปจํŠธ๋กค๋Ÿฌ์—์„œ๋Š” null_session ์‚ฌ์šฉ
  # protect_from_forgery with: :null_session
end

# ๋ทฐ์—์„œ CSRF ํ† ํฐ ์‚ฌ์šฉ
# app/views/posts/new.html.erb
<%= form_with model: @post do |form| %>
  <!-- csrf_meta_tags ํ—ฌํผ๊ฐ€ ์ž๋™์œผ๋กœ ํ† ํฐ ์ถ”๊ฐ€ -->
  <%= form.text_field :title %>
  <%= form.submit %>
<% end %>

# JavaScript์—์„œ CSRF ํ† ํฐ ๊ฐ€์ ธ์˜ค๊ธฐ
# app/javascript/csrf.js
const token = document.querySelector('meta[name="csrf-token"]').content;

fetch('/api/data', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-CSRF-Token': token
  },
  body: JSON.stringify({ data: 'test' })
});
๐Ÿ˜ PHP (Laravel)
<?php
// Laravel๋„ ๊ธฐ๋ณธ์ ์œผ๋กœ CSRF ๋ณดํ˜ธ ํ™œ์„ฑํ™”
// app/Http/Middleware/VerifyCsrfToken.php

namespace App\Http\Middleware;

use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;

class VerifyCsrfToken extends Middleware
{
    // CSRF ๋ณดํ˜ธ ์ œ์™ธํ•  URI
    protected $except = [
        'api/*',
        'webhook/*'
    ];
}

// ๋ธ”๋ ˆ์ด๋“œ ํ…œํ”Œ๋ฆฟ์—์„œ ์‚ฌ์šฉ
// resources/views/form.blade.php
<form method="POST" action="/submit">
    @csrf  <!-- ์ž๋™์œผ๋กœ CSRF ํ† ํฐ ํ•„๋“œ ์ƒ์„ฑ -->
    <input type="text" name="data">
    <button type="submit">์ „์†ก</button>
</form>

// JavaScript์—์„œ ์‚ฌ์šฉ
// resources/js/app.js
axios.defaults.headers.common['X-CSRF-TOKEN'] = 
    document.querySelector('meta[name="csrf-token"]').content;

// API ๋ผ์šฐํŠธ์—์„œ ํ† ํฐ ๊ฐ€์ ธ์˜ค๊ธฐ
Route::get('/csrf-token', function () {
    return response()->json(['token' => csrf_token()]);
});
?>

๋ณด๋‹ค์‹œํ”ผ ๋Œ€๋ถ€๋ถ„์˜ ํ˜„๋Œ€์ ์ธ ํ”„๋ ˆ์ž„์›Œํฌ๋“ค์€ CSRF ๋ณดํ˜ธ๋ฅผ ๊ธฐ๋ณธ์œผ๋กœ ์ œ๊ณตํ•ด ใ…‹ใ…‹ ๊ฐœ๋ฐœ์ž๊ฐ€ ์ง์ ‘ ๊ตฌํ˜„ํ•  ํ•„์š” ์—†์ด ์„ค์ •๋งŒ ์ž˜ ํ•ด์ฃผ๋ฉด ๋ผ. ๊ทผ๋ฐ ์›๋ฆฌ๋ฅผ ์ดํ•ดํ•˜๋Š” ๊ฑด ์—ฌ์ „ํžˆ ์ค‘์š”ํ•ด!

๐ŸŽ“ ๋ณด์•ˆ ๊ฐ•ํ™” ํŒ

๊ธฐ๋ณธ์ ์ธ CSRF ํ† ํฐ๋งŒ์œผ๋กœ๋Š” ๋ถ€์กฑํ•  ๋•Œ๊ฐ€ ์žˆ์–ด. ์ถ”๊ฐ€์ ์ธ ๋ณด์•ˆ ๋ ˆ์ด์–ด๋ฅผ ๋”ํ•˜๋ฉด ๋” ์•ˆ์ „ํ•ด์ง€์ง€ ใ…‡ใ…‡

๋ณด์•ˆ ๊ธฐ๋ฒ• ์„ค๋ช… ๊ตฌํ˜„ ๋‚œ์ด๋„
SameSite ์ฟ ํ‚ค ์ฟ ํ‚ค์— SameSite=Strict ๋˜๋Š” Lax ์„ค์ •์œผ๋กœ CSRF ์ถ”๊ฐ€ ๋ฐฉ์–ด โญ ์‰ฌ์›€
Origin ๊ฒ€์ฆ ์š”์ฒญ์˜ Origin/Referer ํ—ค๋”๋ฅผ ๊ฒ€์ฆํ•˜์—ฌ ์ถœ์ฒ˜ ํ™•์ธ โญโญ ๋ณดํ†ต
์ปค์Šคํ…€ ํ—ค๋” X-Requested-With ๊ฐ™์€ ์ปค์Šคํ…€ ํ—ค๋” ์š”๊ตฌ โญ ์‰ฌ์›€
ํ† ํฐ ์•”ํ˜ธํ™” ํ† ํฐ์„ AES ๋“ฑ์œผ๋กœ ์•”ํ˜ธํ™”ํ•˜์—ฌ ์ €์žฅ โญโญโญ ์–ด๋ ค์›€
Rate Limiting ๋™์ผ IP์—์„œ ๊ณผ๋„ํ•œ ์š”์ฒญ ์ฐจ๋‹จ โญโญ ๋ณดํ†ต
CAPTCHA ์ค‘์š”ํ•œ ์ž‘์—…์— CAPTCHA ์ถ”๊ฐ€ โญโญ ๋ณดํ†ต
๐Ÿ›ก๏ธ ๋‹ค์ธต ๋ฐฉ์–ด ๊ตฌํ˜„ ์˜ˆ์‹œ
const express = require('express');
const rateLimit = require('express-rate-limit');

const app = express();

// 1. Rate Limiting
const limiter = rateLimit({
    windowMs: 15 * 60 * 1000, // 15๋ถ„
    max: 100, // ์ตœ๋Œ€ 100๊ฐœ ์š”์ฒญ
    message: '๋„ˆ๋ฌด ๋งŽ์€ ์š”์ฒญ! ๋‚˜์ค‘์— ๋‹ค์‹œ ์‹œ๋„ํ•˜์„ธ์š” ใ… ใ… '
});

app.use('/api/', limiter);

// 2. Origin ๊ฒ€์ฆ ๋ฏธ๋“ค์›จ์–ด
function verifyOrigin(req, res, next) {
    const allowedOrigins = [
        'https://example.com',
        'https://www.example.com'
    ];
    
    const origin = req.headers.origin || req.headers.referer;
    
    if (!origin || !allowedOrigins.some(allowed => origin.startsWith(allowed))) {
        return res.status(403).json({ 
            error: 'ํ—ˆ์šฉ๋˜์ง€ ์•Š์€ ์ถœ์ฒ˜์ž…๋‹ˆ๋‹ค ๐Ÿšซ' 
        });
    }
    
    next();
}

// 3. ์ปค์Šคํ…€ ํ—ค๋” ๊ฒ€์ฆ
function requireCustomHeader(req, res, next) {
    if (!req.headers['x-requested-with']) {
        return res.status(403).json({ 
            error: 'ํ•„์ˆ˜ ํ—ค๋”๊ฐ€ ๋ˆ„๋ฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค' 
        });
    }
    next();
}

// 4. ์ข…ํ•ฉ ๋ณด์•ˆ ๋ฏธ๋“ค์›จ์–ด
function comprehensiveSecurity(req, res, next) {
    // CSRF ํ† ํฐ ๊ฒ€์ฆ
    const csrfToken = req.headers['x-csrf-token'];
    if (!csrfToken || csrfToken !== req.session.csrfToken) {
        return res.status(403).json({ error: 'CSRF ๊ฒ€์ฆ ์‹คํŒจ' });
    }
    
    // Origin ๊ฒ€์ฆ
    verifyOrigin(req, res, (err) => {
        if (err) return;
        
        // ์ปค์Šคํ…€ ํ—ค๋” ๊ฒ€์ฆ
        requireCustomHeader(req, res, (err) => {
            if (err) return;
            
            // ๋ชจ๋“  ๊ฒ€์ฆ ํ†ต๊ณผ
            next();
        });
    });
}

// ๋ณดํ˜ธ๋œ ๋ผ์šฐํŠธ
app.post('/api/critical-action', 
    limiter,
    comprehensiveSecurity,
    (req, res) => {
        res.json({ success: true, message: '์•ˆ์ „ํ•˜๊ฒŒ ์ฒ˜๋ฆฌ๋จ! ๐ŸŽ‰' });
    }
);
๐Ÿ’ก ์žฌ๋Šฅ๋„ท์—์„œ ๋ณด์•ˆ ์ „๋ฌธ๊ฐ€ ์ฐพ๊ธฐ CSRF ๋ฐฉ์–ด ๊ฐ™์€ ์›น ๋ณด์•ˆ ๊ตฌํ˜„์ด ์–ด๋ ต๋‹ค๋ฉด, ์žฌ๋Šฅ๋„ท์—์„œ ๋ณด์•ˆ ์ „๋ฌธ๊ฐ€์˜ ๋„์›€์„ ๋ฐ›์„ ์ˆ˜ ์žˆ์–ด! ์ฝ”๋“œ ๋ฆฌ๋ทฐ๋ถ€ํ„ฐ ๋ณด์•ˆ ์ปจ์„คํŒ…๊นŒ์ง€ ๋‹ค์–‘ํ•œ ์žฌ๋Šฅ์„ ๊ฑฐ๋ž˜ํ•  ์ˆ˜ ์žˆ๊ฑฐ๋“  ใ…‹ใ…‹ ํ˜ผ์ž ๊ณ ๋ฏผํ•˜์ง€ ๋ง๊ณ  ์ „๋ฌธ๊ฐ€์˜ ๋„์›€์„ ๋ฐ›๋Š” ๊ฒƒ๋„ ์ข‹์€ ๋ฐฉ๋ฒ•์ด์•ผ ใ…‡ใ…‡

๐Ÿ“Š ์„ฑ๋Šฅ ์ตœ์ ํ™”

๋ณด์•ˆ๋„ ์ค‘์š”ํ•˜์ง€๋งŒ ์„ฑ๋Šฅ๋„ ๋ฌด์‹œํ•  ์ˆ˜ ์—†์ง€ ใ…‹ใ…‹ CSRF ํ† ํฐ ๊ฒ€์ฆ์ด ๋ณ‘๋ชฉ์ด ๋˜๋ฉด ์•ˆ ๋˜๋‹ˆ๊นŒ!

โšก ํ† ํฐ ์บ์‹ฑ ์ „๋žต
const NodeCache = require('node-cache');

class OptimizedCSRFManager {
    constructor() {
        // ํ† ํฐ ์บ์‹œ (TTL: 1์‹œ๊ฐ„)
        this.tokenCache = new NodeCache({ 
            stdTTL: 3600,
            checkperiod: 120 
        });
        
        // ๊ฒ€์ฆ ๊ฒฐ๊ณผ ์บ์‹œ (TTL: 5๋ถ„)
        this.validationCache = new NodeCache({ 
            stdTTL: 300,
            checkperiod: 60 
        });
    }
    
    // ํ† ํฐ ์ƒ์„ฑ (์บ์‹œ ํ™œ์šฉ)
    generateToken(sessionId) {
        // ์บ์‹œ์—์„œ ๊ธฐ์กด ํ† ํฐ ํ™•์ธ
        let token = this.tokenCache.get(sessionId);
        
        if (!token) {
            // ์ƒˆ ํ† ํฐ ์ƒ์„ฑ
            token = crypto.randomBytes(32).toString('hex');
            this.tokenCache.set(sessionId, token);
            console.log('์ƒˆ ํ† ํฐ ์ƒ์„ฑ ๋ฐ ์บ์‹ฑ โœจ');
        } else {
            console.log('์บ์‹œ๋œ ํ† ํฐ ์‚ฌ์šฉ โšก');
        }
        
        return token;
    }
    
    // ํ† ํฐ ๊ฒ€์ฆ (์บ์‹œ ํ™œ์šฉ)
    verifyToken(sessionId, token) {
        // ๊ฒ€์ฆ ๊ฒฐ๊ณผ ์บ์‹œ ํ‚ค
        const cacheKey = `${sessionId}:${token}`;
        
        // ์บ์‹œ๋œ ๊ฒ€์ฆ ๊ฒฐ๊ณผ ํ™•์ธ
        let isValid = this.validationCache.get(cacheKey);
        
        if (isValid !== undefined) {
            console.log('์บ์‹œ๋œ ๊ฒ€์ฆ ๊ฒฐ๊ณผ ์‚ฌ์šฉ โšก');
            return isValid;
        }
        
        // ์‹ค์ œ ๊ฒ€์ฆ ์ˆ˜ํ–‰
        const storedToken = this.tokenCache.get(sessionId);
        isValid = storedToken === token;
        
        // ๊ฒ€์ฆ ๊ฒฐ๊ณผ ์บ์‹ฑ
        this.validationCache.set(cacheKey, isValid);
        console.log('๊ฒ€์ฆ ์ˆ˜ํ–‰ ๋ฐ ๊ฒฐ๊ณผ ์บ์‹ฑ ๐Ÿ’พ');
        
        return isValid;
    }
    
    // ํ†ต๊ณ„ ์ •๋ณด
    getStats() {
        return {
            tokenCacheSize: this.tokenCache.keys().length,
            validationCacheSize: this.validationCache.keys().length,
            tokenCacheHits: this.tokenCache.getStats().hits,
            tokenCacheMisses: this.tokenCache.getStats().misses
        };
    }
}

const csrfManager = new OptimizedCSRFManager();

// ์ฃผ๊ธฐ์ ์œผ๋กœ ํ†ต๊ณ„ ๋กœ๊น…
setInterval(() => {
    console.log('๐Ÿ“Š CSRF ์บ์‹œ ํ†ต๊ณ„:', csrfManager.getStats());
}, 60000); // 1๋ถ„๋งˆ๋‹ค
๐Ÿš€ ๋น„๋™๊ธฐ ๊ฒ€์ฆ
// ๋น„๋™๊ธฐ ํ† ํฐ ๊ฒ€์ฆ์œผ๋กœ ์„ฑ๋Šฅ ํ–ฅ์ƒ
async function asyncCSRFVerification(req, res, next) {
    const token = req.headers['x-csrf-token'];
    const sessionId = req.session.id;
    
    // Promise๋กœ ๋น„๋™๊ธฐ ๊ฒ€์ฆ
    const verificationPromise = new Promise((resolve, reject) => {
        setImmediate(() => {
            try {
                const isValid = csrfManager.verifyToken(sessionId, token);
                resolve(isValid);
            } catch (error) {
                reject(error);
            }
        });
    });
    
    try {
        const isValid = await verificationPromise;
        
        if (!isValid) {
            return res.status(403).json({ error: 'CSRF ๊ฒ€์ฆ ์‹คํŒจ' });
        }
        
        next();
    } catch (error) {
        console.error('๊ฒ€์ฆ ์˜ค๋ฅ˜:', error);
        res.status(500).json({ error: '์„œ๋ฒ„ ์˜ค๋ฅ˜' });
    }
}

// ๋ฐฐ์น˜ ๊ฒ€์ฆ (์—ฌ๋Ÿฌ ์š”์ฒญ ๋™์‹œ ์ฒ˜๋ฆฌ)
async function batchVerifyCSRF(requests) {
    const verifications = requests.map(req => ({
        sessionId: req.session.id,
        token: req.headers['x-csrf-token']
    }));
    
    // ๋ณ‘๋ ฌ ๊ฒ€์ฆ
    const results = await Promise.all(
        verifications.map(v => 
            csrfManager.verifyToken(v.sessionId, v.token)
        )
    );
    
    return results;
}

์บ์‹ฑ์„ ์ž˜ ํ™œ์šฉํ•˜๋ฉด ์„ฑ๋Šฅ์ด ์—„์ฒญ ์ข‹์•„์ ธ ใ…‹ใ…‹ ํŠนํžˆ ํŠธ๋ž˜ํ”ฝ์ด ๋งŽ์€ ์„œ๋น„์Šค์—์„œ๋Š” ํ•„์ˆ˜์•ผ. ๊ทผ๋ฐ ์บ์‹œ ๋ฌดํšจํ™” ์ „๋žต๋„ ์ž˜ ์„ธ์›Œ์•ผ ํ•ด!

๐Ÿงช ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ž‘์„ฑ

๋ณด์•ˆ ๊ธฐ๋Šฅ์€ ํ…Œ์ŠคํŠธ๊ฐ€ ์ง„์งœ ์ค‘์š”ํ•ด! CSRF ๋ฐฉ์–ด๊ฐ€ ์ œ๋Œ€๋กœ ์ž‘๋™ํ•˜๋Š”์ง€ ์ž๋™ํ™”๋œ ํ…Œ์ŠคํŠธ๋กœ ๊ฒ€์ฆํ•ด์•ผ ํ•ด.

๐Ÿงช Jest๋ฅผ ์ด์šฉํ•œ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ
// csrf.test.js
const CSRFTokenManager = require('./csrfTokenManager');
const crypto = require('crypto');

describe('CSRF Token Manager', () => {
    let manager;
    
    beforeEach(() => {
        const secretKey = crypto.randomBytes(32).toString('hex');
        manager = new CSRFTokenManager(secretKey);
    });
    
    test('ํ† ํฐ์ด ์ •์ƒ์ ์œผ๋กœ ์ƒ์„ฑ๋˜์–ด์•ผ ํ•จ', () => {
        const sessionId = 'test-session-123';
        const token = manager.generateToken(sessionId);
        
        expect(token).toBeDefined();
        expect(typeof token).toBe('string');
        expect(token.length).toBeGreaterThan(0);
    });
    
    test('์œ ํšจํ•œ ํ† ํฐ์€ ๊ฒ€์ฆ์„ ํ†ต๊ณผํ•ด์•ผ ํ•จ', () => {
        const sessionId = 'test-session-123';
        const token = manager.generateToken(sessionId);
        
        const result = manager.verifyToken(token, sessionId);
        
        expect(result.valid).toBe(true);
    });
    
    test('์ž˜๋ชป๋œ ํ† ํฐ์€ ๊ฒ€์ฆ์„ ์‹คํŒจํ•ด์•ผ ํ•จ', () => {
        const sessionId = 'test-session-123';
        manager.generateToken(sessionId);
        
        const fakeToken = 'fake-token-12345';
        const result = manager.verifyToken(fakeToken, sessionId);
        
        expect(result.valid).toBe(false);
    });
    
    test('๋‹ค๋ฅธ ์„ธ์…˜์˜ ํ† ํฐ์€ ๊ฒ€์ฆ์„ ์‹คํŒจํ•ด์•ผ ํ•จ', () => {
        const sessionId1 = 'session-1';
        const sessionId2 = 'session-2';
        
        const token = manager.generateToken(sessionId1);
        const result = manager.verifyToken(token, sessionId2);
        
        expect(result.valid).toBe(false);
    });
    
    test('๋งŒ๋ฃŒ๋œ ํ† ํฐ์€ ๊ฒ€์ฆ์„ ์‹คํŒจํ•ด์•ผ ํ•จ', () => {
        const sessionId = 'test-session-123';
        const token = manager.generateToken(sessionId);
        
        // ์‹œ๊ฐ„์„ ๋ฏธ๋ž˜๋กœ ์ด๋™ (1์‹œ๊ฐ„ ํ›„)
        jest.useFakeTimers();
        jest.advanceTimersByTime(3600000 + 1000);
        
        const result = manager.verifyToken(token, sessionId, 3600000);
        
        expect(result.valid).toBe(false);
        expect(result.reason).toContain('๋งŒ๋ฃŒ');
        
        jest.useRealTimers();
    });
    
    test('๋™์ผํ•œ ์„ธ์…˜์—์„œ ์—ฌ๋Ÿฌ ํ† ํฐ ์ƒ์„ฑ ๊ฐ€๋Šฅ', () => {
        const sessionId = 'test-session-123';
        
        const token1 = manager.generateToken(sessionId);
        const token2 = manager.generateToken(sessionId);
        
        expect(token1).not.toBe(token2);
        expect(manager.verifyToken(token1, sessionId).valid).toBe(true);
        expect(manager.verifyToken(token2, sessionId).valid).toBe(true);
    });
});
๐ŸŒ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ (Supertest)
// integration.test.js
const request = require('supertest');
const app = require('./app');

describe('CSRF Protection Integration Tests', () => {
    let csrfToken;
    let cookies;
    
    // ํ† ํฐ ๊ฐ€์ ธ์˜ค๊ธฐ
    beforeEach(async () => {
        const response = await request(app)
            .get('/api/csrf-token')
            .expect(200);
        
        csrfToken = response.body.token;
        cookies = response.headers['set-cookie'];
    });
    
    test('GET ์š”์ฒญ์€ CSRF ํ† ํฐ ์—†์ด๋„ ์„ฑ๊ณตํ•ด์•ผ ํ•จ', async () => {
        await request(app)
            .get('/api/data')
            .expect(200);
    });
    
    test('POST ์š”์ฒญ์€ ํ† ํฐ ์—†์ด ์‹คํŒจํ•ด์•ผ ํ•จ', async () => {
        await request(app)
            .post('/api/submit')
            .send({ data: 'test' })
            .expect(403);
    });
    
    test('POST ์š”์ฒญ์€ ์œ ํšจํ•œ ํ† ํฐ๊ณผ ํ•จ๊ป˜ ์„ฑ๊ณตํ•ด์•ผ ํ•จ', async () => {
        await request(app)
            .post('/api/submit')
            .set('Cookie', cookies)
            .set('X-CSRF-Token', csrfToken)
            .send({ data: 'test' })
            .expect(200);
    });
    
    test('์ž˜๋ชป๋œ ํ† ํฐ์œผ๋กœ POST ์š”์ฒญ ์‹œ ์‹คํŒจํ•ด์•ผ ํ•จ', async () => {
        await request(app)
            .post('/api/submit')
            .set('Cookie', cookies)
            .set('X-CSRF-Token', 'wrong-token')
            .send({ data: 'test' })
            .expect(403);
    });
    
    test('ํ† ํฐ ๊ฐฑ์‹ ์ด ์ •์ƒ ์ž‘๋™ํ•ด์•ผ ํ•จ', async () => {
        // ์ฒซ ๋ฒˆ์งธ ํ† ํฐ์œผ๋กœ ์š”์ฒญ
        await request(app)
            .post('/api/submit')
            .set('Cookie', cookies)
            .set('X-CSRF-Token', csrfToken)
            .send({ data: 'test1' })
            .expect(200);
        
        // ์ƒˆ ํ† ํฐ ๋ฐ›๊ธฐ
        const newResponse = await request(app)
            .get('/api/csrf-token')
            .set('Cookie', cookies)
            .expect(200);
        
        const newToken = newResponse.body.token;
        
        // ์ƒˆ ํ† ํฐ์œผ๋กœ ์š”์ฒญ
        await request(app)
            .post('/api/submit')
            .set('Cookie', cookies)
            .set('X-CSRF-Token', newToken)
            .send({ data: 'test2' })
            .expect(200);
    });
});

ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ž‘์„ฑํ•˜๋ฉด ๋ฆฌํŒฉํ† ๋งํ•  ๋•Œ๋„ ์•ˆ์‹ฌ์ด ๋ผ ใ…‹ใ…‹ ๋ญ”๊ฐ€ ๋ฐ”๊ฟจ๋Š”๋ฐ ๋ณด์•ˆ์ด ๋šซ๋ ธ๋‹ค? ๊ทธ๋Ÿฐ ์ผ ์—†๊ฒŒ ํ•ด์ฃผ๊ฑฐ๋“ !

๐ŸŽฏ ์‹ค์ „ ์ฒดํฌ๋ฆฌ์ŠคํŠธ

๋งˆ์ง€๋ง‰์œผ๋กœ CSRF ๋ฐฉ์–ด ๊ตฌํ˜„ํ•  ๋•Œ ๊ผญ ํ™•์ธํ•ด์•ผ ํ•  ์ฒดํฌ๋ฆฌ์ŠคํŠธ ์ •๋ฆฌํ•ด์ค„๊ฒŒ!

โœ… CSRF ๋ฐฉ์–ด ๊ตฌํ˜„ ์ฒดํฌ๋ฆฌ์ŠคํŠธ ๐Ÿ” ํ† ํฐ ์ƒ์„ฑ
โ˜ ์•”ํ˜ธํ•™์ ์œผ๋กœ ์•ˆ์ „ํ•œ ๋‚œ์ˆ˜ ์ƒ์„ฑ๊ธฐ ์‚ฌ์šฉ (crypto.randomBytes ๋“ฑ)
โ˜ ์ถฉ๋ถ„ํ•œ ๊ธธ์ด์˜ ํ† ํฐ (์ตœ์†Œ 128๋น„ํŠธ)
โ˜ ์˜ˆ์ธก ๋ถˆ๊ฐ€๋Šฅํ•œ ํ† ํฐ ์ƒ์„ฑ
โ˜ ์„ธ์…˜/์‚ฌ์šฉ์ž๋ณ„๋กœ ๊ณ ์œ ํ•œ ํ† ํฐ

๐Ÿ›ก๏ธ ํ† ํฐ ์ €์žฅ
โ˜ ์„œ๋ฒ„ ์ธก์— ์•ˆ์ „ํ•˜๊ฒŒ ์ €์žฅ (์„ธ์…˜, DB, ์บ์‹œ)
โ˜ ํด๋ผ์ด์–ธํŠธ์—๋Š” ์ฟ ํ‚ค ๋˜๋Š” ํžˆ๋“  ํ•„๋“œ๋กœ ์ „๋‹ฌ
โ˜ httpOnly, secure, sameSite ์ฟ ํ‚ค ์†์„ฑ ์„ค์ •
โ˜ ์ ์ ˆํ•œ ๋งŒ๋ฃŒ ์‹œ๊ฐ„ ์„ค์ •

โœ”๏ธ ํ† ํฐ ๊ฒ€์ฆ
โ˜ ๋ชจ๋“  ์ƒํƒœ ๋ณ€๊ฒฝ ์š”์ฒญ(POST, PUT, DELETE)์— ๊ฒ€์ฆ ์ ์šฉ
โ˜ GET ์š”์ฒญ์€ ๊ฒ€์ฆํ•˜์ง€ ์•Š์Œ
โ˜ ํ† ํฐ ์ผ์น˜ ์—ฌ๋ถ€ ํ™•์ธ
โ˜ ํ† ํฐ ๋งŒ๋ฃŒ ์—ฌ๋ถ€ ํ™•์ธ
โ˜ ๊ฒ€์ฆ ์‹คํŒจ ์‹œ ์ ์ ˆํ•œ ์—๋Ÿฌ ์‘๋‹ต (403 Forbidden)

๐ŸŒ CORS ๋ฐ ํ—ค๋”
โ˜ CORS ์„ค์ • ํ™•์ธ
โ˜ Origin/Referer ํ—ค๋” ๊ฒ€์ฆ
โ˜ ์ปค์Šคํ…€ ํ—ค๋” ํ™œ์šฉ (X-Requested-With ๋“ฑ)
โ˜ Content-Type ๊ฒ€์ฆ

๐Ÿ”„ ํ† ํฐ ๊ด€๋ฆฌ
โ˜ ํ† ํฐ ๊ฐฑ์‹  ๋ฉ”์ปค๋‹ˆ์ฆ˜ ๊ตฌํ˜„
โ˜ ๋กœ๊ทธ์•„์›ƒ ์‹œ ํ† ํฐ ๋ฌดํšจํ™”
โ˜ ์„ธ์…˜ ๋งŒ๋ฃŒ ์‹œ ํ† ํฐ ์‚ญ์ œ
โ˜ ํ† ํฐ ์žฌ์‚ฌ์šฉ ๋ฐฉ์ง€ (ํ•„์š”์‹œ)

๐Ÿงช ํ…Œ์ŠคํŠธ
โ˜ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ์ž‘์„ฑ
โ˜ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ์ž‘์„ฑ
โ˜ ๋ณด์•ˆ ํ…Œ์ŠคํŠธ ์ˆ˜ํ–‰
โ˜ ๋‹ค์–‘ํ•œ ๊ณต๊ฒฉ ์‹œ๋‚˜๋ฆฌ์˜ค ํ…Œ์ŠคํŠธ

๐Ÿ“Š ๋ชจ๋‹ˆํ„ฐ๋ง
โ˜ CSRF ๊ณต๊ฒฉ ์‹œ๋„ ๋กœ๊น…
โ˜ ๊ฒ€์ฆ ์‹คํŒจ ๋ชจ๋‹ˆํ„ฐ๋ง
โ˜ ์„ฑ๋Šฅ ๋ฉ”ํŠธ๋ฆญ ์ˆ˜์ง‘
โ˜ ์•Œ๋ฆผ ์„ค์ •

๐Ÿ“š ๋ฌธ์„œํ™”
โ˜ API ๋ฌธ์„œ์— CSRF ํ† ํฐ ์š”๊ตฌ์‚ฌํ•ญ ๋ช…์‹œ
โ˜ ํด๋ผ์ด์–ธํŠธ ๊ฐœ๋ฐœ์ž๋ฅผ ์œ„ํ•œ ๊ฐ€์ด๋“œ ์ž‘์„ฑ
โ˜ ์—๋Ÿฌ ์ฝ”๋“œ ๋ฐ ๋ฉ”์‹œ์ง€ ๋ฌธ์„œํ™”
โ˜ ๋ณด์•ˆ ์ •์ฑ… ๋ฌธ์„œํ™”

๐ŸŽฌ ๋งˆ๋ฌด๋ฆฌํ•˜๋ฉฐ

์™€ ์—ฌ๊ธฐ๊นŒ์ง€ ์ฝ์—ˆ์œผ๋ฉด ์ง„์งœ ๋Œ€๋‹จํ•œ ๊ฑฐ์•ผ ใ…‹ใ…‹ใ…‹ CSRF ๋ฐฉ์–ด์— ๋Œ€ํ•ด ๊ฝค ๋งŽ์ด ์•Œ๊ฒŒ ๋์„ ๊ฑฐ์•ผ!

ํ•ต์‹ฌ๋งŒ ๋‹ค์‹œ ์ •๋ฆฌํ•˜๋ฉด:
1๏ธโƒฃ CSRF๋Š” ์‚ฌ์šฉ์ž ๋ชจ๋ฅด๊ฒŒ ์•…์˜์ ์ธ ์š”์ฒญ์„ ๋ณด๋‚ด๋Š” ๊ณต๊ฒฉ
2๏ธโƒฃ ํ† ํฐ ๊ธฐ๋ฐ˜ ๋ฐฉ์–ด๊ฐ€ ๊ฐ€์žฅ ํšจ๊ณผ์ 
3๏ธโƒฃ ์•”ํ˜ธํ•™์ ์œผ๋กœ ์•ˆ์ „ํ•œ ๋žœ๋ค ํ† ํฐ ์ƒ์„ฑ ํ•„์ˆ˜
4๏ธโƒฃ ๋ชจ๋“  ์ƒํƒœ ๋ณ€๊ฒฝ ์š”์ฒญ์— ํ† ํฐ ๊ฒ€์ฆ ์ ์šฉ
5๏ธโƒฃ ํ”„๋ ˆ์ž„์›Œํฌ ๋‚ด์žฅ ๊ธฐ๋Šฅ ์ ๊ทน ํ™œ์šฉ
6๏ธโƒฃ ๋‹ค์ธต ๋ฐฉ์–ด ์ „๋žต์œผ๋กœ ๋ณด์•ˆ ๊ฐ•ํ™”
7๏ธโƒฃ ํ…Œ์ŠคํŠธ์™€ ๋ชจ๋‹ˆํ„ฐ๋ง ํ•„์ˆ˜!

๋ณด์•ˆ์€ ํ•œ ๋ฒˆ ๊ตฌํ˜„ํ•˜๊ณ  ๋์ด ์•„๋‹ˆ์•ผ. ์ง€์†์ ์œผ๋กœ ์—…๋ฐ์ดํŠธํ•˜๊ณ  ๋ชจ๋‹ˆํ„ฐ๋งํ•ด์•ผ ํ•ด. ์ƒˆ๋กœ์šด ๊ณต๊ฒฉ ๊ธฐ๋ฒ•๋„ ๊ณ„์† ๋‚˜์˜ค๋‹ˆ๊นŒ ๋ง์ด์ง€ ใ…‡ใ…‡

ํ˜น์‹œ ๊ตฌํ˜„ํ•˜๋‹ค๊ฐ€ ๋ง‰ํžˆ๊ฑฐ๋‚˜ ๋„์›€์ด ํ•„์š”ํ•˜๋ฉด ์žฌ๋Šฅ๋„ท์—์„œ ์ „๋ฌธ๊ฐ€๋“ค์˜ ๋„์›€์„ ๋ฐ›๋Š” ๊ฒƒ๋„ ์ข‹์€ ๋ฐฉ๋ฒ•์ด์•ผ. ํ˜ผ์ž ๊ณ ๋ฏผํ•˜์ง€ ๋ง๊ณ  ํ•จ๊ป˜ ํ•ด๊ฒฐํ•ด๋‚˜๊ฐ€์ž๊ตฌ!

๊ทธ๋Ÿผ ๋ชจ๋‘ ์•ˆ์ „ํ•œ ์ฝ”๋”ฉ ํ•˜์‹œ๊ธธ ๋ฐ”๋ž˜์š”! ํ™”์ดํŒ… ๐Ÿš€โœจ

๐ŸŽ“ ์ด ๊ธ€์ด ๋„์›€์ด ๋˜์—ˆ๋‚˜์š”?

CSRF ๋ฐฉ์–ด๋ถ€ํ„ฐ ๋‹ค์–‘ํ•œ ๋ณด์•ˆ ๊ธฐ์ˆ ๊นŒ์ง€,
๋” ๋งŽ์€ ๊ฐœ๋ฐœ ์ง€์‹์€ ์žฌ๋Šฅ๋„ท ์ง€์‹์ธ์˜ ์ˆฒ์—์„œ ๋งŒ๋‚˜๋ณด์„ธ์š”!
ํ”„๋กœ๊ทธ๋ž˜๋ฐ ๊ณ ์ˆ˜๋“ค์˜ ๋…ธํ•˜์šฐ๊ฐ€ ๊ฐ€๋“ํ•ฉ๋‹ˆ๋‹ค โœจ

๋Œ“๊ธ€ ์ž‘์„ฑ

์ด ๊ธ€์— ๋Œ€ํ•œ ์—ฌ๋Ÿฌ๋ถ„์˜ ์ƒ๊ฐ์„ ๋“ค๋ ค์ฃผ์„ธ์š”

๋Œ“๊ธ€ 0
๋Œ“๊ธ€์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘์ž…๋‹ˆ๋‹ค.

์•„์ง ๋Œ“๊ธ€์ด ์—†์Šต๋‹ˆ๋‹ค.