์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐Ÿ” PHP ์„ธ์…˜ ๊ด€๋ฆฌ์™€ ์ฟ ํ‚ค ๋ณด์•ˆ์œผ๋กœ ์‚ฌ์šฉ์ž ์ƒํƒœ๋ฅผ ์™„๋ฒฝํ•˜๊ฒŒ ์ง€ํ‚ค๋Š” ๋ฐฉ๋ฒ•

๐Ÿ” PHP ์„ธ์…˜ ๊ด€๋ฆฌ์™€ ์ฟ ํ‚ค ๋ณด์•ˆ์œผ๋กœ ์‚ฌ์šฉ์ž ์ƒํƒœ๋ฅผ ์™„๋ฒฝํ•˜๊ฒŒ ์ง€ํ‚ค๋Š” ๋ฐฉ๋ฒ•

์›น ๊ฐœ๋ฐœ์ž๋ผ๋ฉด ๋ฐ˜๋“œ์‹œ ์•Œ์•„์•ผ ํ•  ์‚ฌ์šฉ์ž ์ธ์ฆ๊ณผ ๋ณด์•ˆ ํ•ต์‹ฌ ๊ฐ€์ด๋“œ

์•ˆ๋…•! ๐Ÿ‘‹ ์˜ค๋Š˜์€ PHP๋กœ ์›น ๊ฐœ๋ฐœํ•  ๋•Œ ์ •๋ง ์ค‘์š”ํ•œ ์ฃผ์ œ๋ฅผ ๋‹ค๋ค„๋ณผ ๊ฑฐ์•ผ. ๋ฐ”๋กœ ์„ธ์…˜ ๊ด€๋ฆฌ์™€ ์ฟ ํ‚ค ๋ณด์•ˆ์ด์ง€. ์‚ฌ์‹ค ์ด๊ฑฐ, ์ฒ˜์Œ ๋ฐฐ์šธ ๋• "๊ทธ๋ƒฅ ๋กœ๊ทธ์ธ ๊ธฐ๋Šฅ ๋งŒ๋“œ๋Š” ๊ฑฐ ์•„๋‹ˆ์•ผ?"๋ผ๊ณ  ์ƒ๊ฐํ•˜๊ธฐ ์‰ฌ์šด๋ฐ, ๋ง‰์ƒ ์ œ๋Œ€๋กœ ํŒŒ๊ณ ๋“ค๋ฉด ๋ณด์•ˆ ์ทจ์•ฝ์ ์ด ํ•œ๋‘ ๊ฐœ๊ฐ€ ์•„๋‹ˆ๋”๋ผ๊ณ . ๐Ÿ˜…

์›น์‚ฌ์ดํŠธ์—์„œ ๋กœ๊ทธ์ธํ•˜๋ฉด ํŽ˜์ด์ง€๋ฅผ ์ด๋™ํ•ด๋„ ๋กœ๊ทธ์ธ ์ƒํƒœ๊ฐ€ ์œ ์ง€๋˜์ž–์•„? ์ด๊ฒŒ ๋ฐ”๋กœ ์„ธ์…˜๊ณผ ์ฟ ํ‚ค ๋•๋ถ„์ด์•ผ. ๊ทผ๋ฐ ์ด๊ฑธ ์ž˜๋ชป ๊ตฌํ˜„ํ•˜๋ฉด ํ•ด์ปค๋“ค์ด ์‚ฌ์šฉ์ž ๊ณ„์ •์„ ํƒˆ์ทจํ•˜๊ฑฐ๋‚˜, ๊ฐœ์ธ์ •๋ณด๋ฅผ ๋นผ๊ฐ€๋Š” ์ผ์ด ์ƒ๊ธธ ์ˆ˜ ์žˆ์–ด. ์‹ค์ œ๋กœ ๋งŽ์€ ์›น์‚ฌ์ดํŠธ๋“ค์ด ์ด๋Ÿฐ ๋ณด์•ˆ ๋ฌธ์ œ๋กœ ๊ณค๋ž€์„ ๊ฒช๊ณ  ์žˆ์ง€.

ํด๋ผ์ด์–ธํŠธ ๋ธŒ๋ผ์šฐ์ € ์ฟ ํ‚ค ์ €์žฅ์†Œ ์„œ๋ฒ„ PHP ์„ธ์…˜ ์ €์žฅ์†Œ ์š”์ฒญ + ์ฟ ํ‚ค ์‘๋‹ต + Set-Cookie ์„ธ์…˜ ID ๊ตํ™˜ 1. ์„œ๋ฒ„๊ฐ€ ์„ธ์…˜ ID ์ƒ์„ฑ 2. ์ฟ ํ‚ค๋กœ ํด๋ผ์ด์–ธํŠธ์— ์ „์†ก 3. ์ดํ›„ ์š”์ฒญ๋งˆ๋‹ค ์„ธ์…˜ ID ํฌํ•จ

๐Ÿช ์ฟ ํ‚ค(Cookie)๋ž€ ๋ฌด์—‡์ธ๊ฐ€?

๋จผ์ € ์ฟ ํ‚ค๋ถ€ํ„ฐ ์•Œ์•„๋ณด์ž. ์ฟ ํ‚ค๋Š” ์›น ๋ธŒ๋ผ์šฐ์ €์— ์ €์žฅ๋˜๋Š” ์ž‘์€ ํ…์ŠคํŠธ ํŒŒ์ผ์ด์•ผ. ์„œ๋ฒ„๊ฐ€ "์ด ์ •๋ณด ์ข€ ์ €์žฅํ•ด๋‘ฌ!"๋ผ๊ณ  ๋ธŒ๋ผ์šฐ์ €์—๊ฒŒ ๋ถ€ํƒํ•˜๋ฉด, ๋ธŒ๋ผ์šฐ์ €๋Š” ๊ทธ๊ฑธ ์ฟ ํ‚ค ํ˜•ํƒœ๋กœ ์ €์žฅํ•ด๋‘๋Š” ๊ฑฐ์ง€. ๊ทธ๋ฆฌ๊ณ  ๋‹ค์Œ์— ๊ฐ™์€ ์‚ฌ์ดํŠธ๋ฅผ ๋ฐฉ๋ฌธํ•  ๋•Œ ์ž๋™์œผ๋กœ ๊ทธ ์ฟ ํ‚ค๋ฅผ ์„œ๋ฒ„์— ๋ณด๋‚ด์ค˜.

์˜ˆ๋ฅผ ๋“ค์–ด๋ณผ๊ฒŒ. ๋„ค๊ฐ€ ์‡ผํ•‘๋ชฐ์—์„œ ์žฅ๋ฐ”๊ตฌ๋‹ˆ์— ์ƒํ’ˆ์„ ๋‹ด์•˜๋Š”๋ฐ, ๊ฐ‘์ž๊ธฐ ๋ธŒ๋ผ์šฐ์ €๋ฅผ ๋‹ซ์•˜๋‹ค๊ฐ€ ๋‹ค์‹œ ์—ด์—ˆ์–ด. ๊ทธ๋Ÿฐ๋ฐ๋„ ์žฅ๋ฐ”๊ตฌ๋‹ˆ์— ์ƒํ’ˆ์ด ๊ทธ๋Œ€๋กœ ์žˆ์ง€? ์ด๊ฒŒ ๋ฐ”๋กœ ์ฟ ํ‚ค ๋•๋ถ„์ด์•ผ! ๐Ÿ›’

๐Ÿ“Œ ์ฟ ํ‚ค์˜ ์ฃผ์š” ํŠน์ง• โ€ข ํด๋ผ์ด์–ธํŠธ ์ธก ์ €์žฅ: ์‚ฌ์šฉ์ž์˜ ๋ธŒ๋ผ์šฐ์ €์— ์ €์žฅ๋จ
โ€ข ํฌ๊ธฐ ์ œํ•œ: ๋ณดํ†ต 4KB ์ •๋„๋กœ ์ž‘์€ ๋ฐ์ดํ„ฐ๋งŒ ์ €์žฅ ๊ฐ€๋Šฅ
โ€ข ๋งŒ๋ฃŒ ์‹œ๊ฐ„: ์„ค์ •ํ•œ ์‹œ๊ฐ„์ด ์ง€๋‚˜๋ฉด ์ž๋™์œผ๋กœ ์‚ญ์ œ๋จ
โ€ข ๋„๋ฉ”์ธ ์ œํ•œ: ์ฟ ํ‚ค๋ฅผ ์ƒ์„ฑํ•œ ๋„๋ฉ”์ธ์—์„œ๋งŒ ์ ‘๊ทผ ๊ฐ€๋Šฅ
โ€ข ์ž๋™ ์ „์†ก: ํ•ด๋‹น ๋„๋ฉ”์ธ์— ์š”์ฒญํ•  ๋•Œ๋งˆ๋‹ค ์ž๋™์œผ๋กœ ์ „์†ก๋จ

PHP์—์„œ ์ฟ ํ‚ค ์„ค์ •ํ•˜๊ธฐ

PHP์—์„œ ์ฟ ํ‚ค๋ฅผ ์„ค์ •ํ•˜๋Š” ๊ฑด ์ •๋ง ๊ฐ„๋‹จํ•ด. setcookie() ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋ผ. ๊ทผ๋ฐ ์—ฌ๊ธฐ์„œ ์ค‘์š”ํ•œ ๊ฑด, ์ด ํ•จ์ˆ˜๋Š” ๋ฐ˜๋“œ์‹œ HTML ์ถœ๋ ฅ ์ „์— ํ˜ธ์ถœํ•ด์•ผ ํ•œ๋‹ค๋Š” ๊ฑฐ์•ผ. ์™œ๋ƒํ•˜๋ฉด ์ฟ ํ‚ค๋Š” HTTP ํ—ค๋”๋กœ ์ „์†ก๋˜๊ฑฐ๋“ .

<?php
// ๊ธฐ๋ณธ ์ฟ ํ‚ค ์„ค์ •
setcookie("username", "ํ™๊ธธ๋™", time() + 3600); // 1์‹œ๊ฐ„ ๋™์•ˆ ์œ ํšจ

// ๋” ์•ˆ์ „ํ•œ ์ฟ ํ‚ค ์„ค์ • (๋ณด์•ˆ ์˜ต์…˜ ํฌํ•จ)
setcookie(
    "user_token",           // ์ฟ ํ‚ค ์ด๋ฆ„
    "abc123xyz",            // ์ฟ ํ‚ค ๊ฐ’
    time() + 86400,         // ๋งŒ๋ฃŒ ์‹œ๊ฐ„ (24์‹œ๊ฐ„)
    "/",                    // ๊ฒฝ๋กœ (์ „์ฒด ์‚ฌ์ดํŠธ)
    "",                     // ๋„๋ฉ”์ธ
    true,                   // HTTPS์—์„œ๋งŒ ์ „์†ก
    true                    // JavaScript ์ ‘๊ทผ ์ฐจ๋‹จ (HttpOnly)
);

// ์ฟ ํ‚ค ์ฝ๊ธฐ
if(isset($_COOKIE['username'])) {
    echo "ํ™˜์˜ํ•ฉ๋‹ˆ๋‹ค, " . htmlspecialchars($_COOKIE['username']) . "๋‹˜!";
}

// ์ฟ ํ‚ค ์‚ญ์ œ
setcookie("username", "", time() - 3600);
?>
โš ๏ธ ๋ณด์•ˆ ์ฃผ์˜์‚ฌํ•ญ ์ฟ ํ‚ค๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ์ง์ ‘ ์ˆ˜์ •ํ•  ์ˆ˜ ์žˆ์–ด! ๋ธŒ๋ผ์šฐ์ € ๊ฐœ๋ฐœ์ž ๋„๊ตฌ๋งŒ ์—ด๋ฉด ์ฟ ํ‚ค ๊ฐ’์„ ๋งˆ์Œ๋Œ€๋กœ ๋ฐ”๊ฟ€ ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฑฐ์ง€. ๊ทธ๋ž˜์„œ ๋ฏผ๊ฐํ•œ ์ •๋ณด๋Š” ์ ˆ๋Œ€ ์ฟ ํ‚ค์— ์ €์žฅํ•˜๋ฉด ์•ˆ ๋ผ. ๋น„๋ฐ€๋ฒˆํ˜ธ, ์‹ ์šฉ์นด๋“œ ๋ฒˆํ˜ธ ๊ฐ™์€ ๊ฑด ๋‹น์—ฐํžˆ ์•ˆ ๋˜๊ณ , ์‚ฌ์šฉ์ž ๊ถŒํ•œ ์ •๋ณด๋„ ์ฟ ํ‚ค์— ์ง์ ‘ ๋„ฃ์œผ๋ฉด ์œ„ํ—˜ํ•ด.

๐ŸŽซ ์„ธ์…˜(Session)์ด๋ž€ ๋ฌด์—‡์ธ๊ฐ€?

์ž, ์ด์ œ ์„ธ์…˜ ์–˜๊ธฐ๋ฅผ ํ•ด๋ณด์ž. ์ฟ ํ‚ค์˜ ๋ณด์•ˆ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋‚˜์˜จ ๊ฒŒ ๋ฐ”๋กœ ์„ธ์…˜์ด์•ผ. ์„ธ์…˜์€ ์„œ๋ฒ„ ์ธก์— ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•˜๋Š” ๋ฐฉ์‹์ด๊ฑฐ๋“ . ์‚ฌ์šฉ์ž ๋ธŒ๋ผ์šฐ์ €์—๋Š” ์„ธ์…˜ ID๋งŒ ์ฟ ํ‚ค๋กœ ์ €์žฅํ•˜๊ณ , ์‹ค์ œ ๋ฐ์ดํ„ฐ๋Š” ์„œ๋ฒ„์— ์•ˆ์ „ํ•˜๊ฒŒ ๋ณด๊ด€ํ•˜๋Š” ๊ฑฐ์ง€. ๐Ÿ”’

์ƒ๊ฐํ•ด๋ด. ์€ํ–‰์— ๊ฐ€๋ฉด ๋ฒˆํ˜ธํ‘œ๋ฅผ ๋ฐ›์ž–์•„? ๊ทธ ๋ฒˆํ˜ธํ‘œ ์ž์ฒด๋Š” ์•„๋ฌด ์˜๋ฏธ ์—†์–ด. ํ•˜์ง€๋งŒ ์€ํ–‰ ์‹œ์Šคํ…œ์—์„œ๋Š” ๊ทธ ๋ฒˆํ˜ธ์™€ ์—ฐ๊ฒฐ๋œ ๊ณ ๊ฐ ์ •๋ณด๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ์ง€. ์„ธ์…˜๋„ ๋˜‘๊ฐ™์•„! ์„ธ์…˜ ID๋Š” ๊ทธ๋ƒฅ ๋ฒˆํ˜ธํ‘œ๊ณ , ์‹ค์ œ ์ •๋ณด๋Š” ์„œ๋ฒ„์— ์žˆ๋Š” ๊ฑฐ์•ผ.

PHP ์„ธ์…˜์˜ ์ž‘๋™ ์›๋ฆฌ

<?php
// ์„ธ์…˜ ์‹œ์ž‘ (ํ•ญ์ƒ ๋งจ ์ฒ˜์Œ์—!)
session_start();

// ์„ธ์…˜์— ๋ฐ์ดํ„ฐ ์ €์žฅ
$_SESSION['user_id'] = 12345;
$_SESSION['username'] = 'ํ™๊ธธ๋™';
$_SESSION['role'] = 'admin';
$_SESSION['login_time'] = time();

// ์„ธ์…˜ ๋ฐ์ดํ„ฐ ์ฝ๊ธฐ
if(isset($_SESSION['username'])) {
    echo "๋กœ๊ทธ์ธ ์‚ฌ์šฉ์ž: " . htmlspecialchars($_SESSION['username']);
}

// ํŠน์ • ์„ธ์…˜ ๋ณ€์ˆ˜ ์‚ญ์ œ
unset($_SESSION['login_time']);

// ๋ชจ๋“  ์„ธ์…˜ ๋ฐ์ดํ„ฐ ์‚ญ์ œ
session_unset();

// ์„ธ์…˜ ์™„์ „ํžˆ ์ข…๋ฃŒ (๋กœ๊ทธ์•„์›ƒ)
session_destroy();
?>
๐Ÿ’ก Pro Tip session_start()๋Š” ํŽ˜์ด์ง€ ์ตœ์ƒ๋‹จ์—์„œ ํ˜ธ์ถœํ•ด์•ผ ํ•ด. HTML ํƒœ๊ทธ๋‚˜ ๊ณต๋ฐฑ, ์‹ฌ์ง€์–ด BOM(Byte Order Mark)๋„ ์žˆ์œผ๋ฉด ์•ˆ ๋ผ. "Headers already sent" ์—๋Ÿฌ๊ฐ€ ๋‚˜๋ฉด ๋Œ€๋ถ€๋ถ„ ์ด๊ฒŒ ์›์ธ์ด์•ผ!

์„ธ์…˜ ์ €์žฅ ์œ„์น˜

PHP๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ ์„ธ์…˜ ๋ฐ์ดํ„ฐ๋ฅผ ์„œ๋ฒ„์˜ ์ž„์‹œ ๋””๋ ‰ํ† ๋ฆฌ์— ํŒŒ์ผ๋กœ ์ €์žฅํ•ด. ๋ฆฌ๋ˆ…์Šค๋ผ๋ฉด ๋ณดํ†ต /tmp ๋””๋ ‰ํ† ๋ฆฌ์— sess_์„ธ์…˜ID ํ˜•ํƒœ๋กœ ์ €์žฅ๋˜์ง€. ํ•˜์ง€๋งŒ ํŠธ๋ž˜ํ”ฝ์ด ๋งŽ์€ ์‚ฌ์ดํŠธ๋ผ๋ฉด ์ด ๋ฐฉ์‹์€ ๋น„ํšจ์œจ์ ์ด์•ผ.

์ €์žฅ ๋ฐฉ์‹ ์žฅ์  ๋‹จ์  ์ถ”์ฒœ ์ƒํ™ฉ
ํŒŒ์ผ ์„ค์ • ๊ฐ„๋‹จ, ๊ธฐ๋ณธ๊ฐ’ ์†๋„ ๋А๋ฆผ, ํ™•์žฅ์„ฑ ๋‚ฎ์Œ ์†Œ๊ทœ๋ชจ ์‚ฌ์ดํŠธ
๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์˜๊ตฌ ๋ณด๊ด€, ๊ด€๋ฆฌ ์šฉ์ด DB ๋ถ€ํ•˜ ์ฆ๊ฐ€ ์ค‘๊ทœ๋ชจ ์‚ฌ์ดํŠธ
Redis/Memcached ๋งค์šฐ ๋น ๋ฆ„, ํ™•์žฅ์„ฑ ์šฐ์ˆ˜ ๋ณ„๋„ ์„œ๋ฒ„ ํ•„์š” ๋Œ€๊ทœ๋ชจ ์‚ฌ์ดํŠธ

์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ๋„ ์‚ฌ์šฉ์ž๊ฐ€ ๋งŽ์•„์ง€๋ฉด ์„ธ์…˜ ๊ด€๋ฆฌ๊ฐ€ ์ •๋ง ์ค‘์š”ํ•ด์ ธ. ์ˆ˜์ฒœ ๋ช…์ด ๋™์‹œ์— ์ ‘์†ํ•˜๋Š” ์ƒํ™ฉ์—์„œ ํŒŒ์ผ ๊ธฐ๋ฐ˜ ์„ธ์…˜์„ ์“ฐ๋ฉด ์„œ๋ฒ„๊ฐ€ ๋ฒ„ํ‹ฐ๊ธฐ ํž˜๋“ค๊ฑฐ๋“ . ๊ทธ๋ž˜์„œ Redis ๊ฐ™์€ ์ธ๋ฉ”๋ชจ๋ฆฌ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒŒ ์ผ๋ฐ˜์ ์ด์•ผ. โšก

๐Ÿ›ก๏ธ ์„ธ์…˜ ๋ณด์•ˆ: ์„ธ์…˜ ํ•˜์ด์žฌํ‚น ๋ฐฉ์–ดํ•˜๊ธฐ

์ž, ์ด์ œ ์ง„์งœ ์ค‘์š”ํ•œ ๋ณด์•ˆ ์–˜๊ธฐ๋ฅผ ํ•ด๋ณด์ž. ์„ธ์…˜ ํ•˜์ด์žฌํ‚น(Session Hijacking)์ด๋ผ๊ณ  ๋“ค์–ด๋ดค์–ด? ํ•ด์ปค๊ฐ€ ๋‹ค๋ฅธ ์‚ฌ๋žŒ์˜ ์„ธ์…˜ ID๋ฅผ ํ›”์ณ์„œ ๊ทธ ์‚ฌ๋žŒ์ธ ์ฒ™ ํ•˜๋Š” ๊ณต๊ฒฉ์ด์•ผ. ๐Ÿ˜ฑ

์ƒ์ƒํ•ด๋ด. ๋„ค๊ฐ€ ์นดํŽ˜์—์„œ ๊ณต์šฉ ์™€์ดํŒŒ์ด๋กœ ๋กœ๊ทธ์ธํ–ˆ๋Š”๋ฐ, ์˜†์— ์žˆ๋˜ ํ•ด์ปค๊ฐ€ ๋„คํŠธ์›Œํฌ ํŒจํ‚ท์„ ๊ฐ€๋กœ์ฑ„์„œ ์„ธ์…˜ ID๋ฅผ ํ›”์ณค์–ด. ๊ทธ๋Ÿผ ๊ทธ ํ•ด์ปค๋Š” ๋„ค ๊ณ„์ •์œผ๋กœ ๋ญ๋“ ์ง€ ํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋˜๋Š” ๊ฑฐ์ง€. ๋ฌด์„ญ์ง€?

์„ธ์…˜ ํ•˜์ด์žฌํ‚น ๊ณต๊ฒฉ ์‹œ๋‚˜๋ฆฌ์˜ค ์ •์ƒ ์‚ฌ์šฉ์ž ์„œ๋ฒ„ ํ•ด์ปค ์„ธ์…˜ ID: ABC123 ์„ธ์…˜ ID ํƒˆ์ทจ! ํ›”์นœ ์„ธ์…˜์œผ๋กœ ์ ‘๊ทผ ๋ฐฉ์–ด ๋ฐฉ๋ฒ• 1. HTTPS ์‚ฌ์šฉ์œผ๋กœ ์•”ํ˜ธํ™” 2. ์„ธ์…˜ ID ์ •๊ธฐ์  ์žฌ์ƒ์„ฑ 3. IP ์ฃผ์†Œ ๋ฐ User-Agent ๊ฒ€์ฆ

1. HTTPS ์‚ฌ์šฉ (ํ•„์ˆ˜!)

์ฒซ ๋ฒˆ์งธ ๋ฐฉ์–ด์„ ์€ HTTPS์•ผ. HTTP๋Š” ํ‰๋ฌธ์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ์ „์†กํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๋ˆ„๊ตฌ๋‚˜ ๊ฐ€๋กœ์ฑŒ ์ˆ˜ ์žˆ์–ด. ํ•˜์ง€๋งŒ HTTPS๋Š” ๋ชจ๋“  ํ†ต์‹ ์„ ์•”ํ˜ธํ™”ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์ค‘๊ฐ„์—์„œ ๊ฐ€๋กœ์ฑ„๋„ ๋‚ด์šฉ์„ ์•Œ ์ˆ˜ ์—†์ง€. ์š”์ฆ˜์€ Let's Encrypt ๋•๋ถ„์— ๋ฌด๋ฃŒ๋กœ SSL ์ธ์ฆ์„œ๋ฅผ ๋ฐ›์„ ์ˆ˜ ์žˆ์œผ๋‹ˆ๊นŒ, HTTPS๋Š” ํ•„์ˆ˜์•ผ!

<?php
// php.ini ์„ค์ • ๋˜๋Š” ์ฝ”๋“œ์—์„œ ์ง์ ‘ ์„ค์ •
ini_set('session.cookie_secure', '1');     // HTTPS์—์„œ๋งŒ ์ฟ ํ‚ค ์ „์†ก
ini_set('session.cookie_httponly', '1');   // JavaScript ์ ‘๊ทผ ์ฐจ๋‹จ
ini_set('session.cookie_samesite', 'Strict'); // CSRF ๊ณต๊ฒฉ ๋ฐฉ์–ด

// ๋˜๋Š” session_start() ์˜ต์…˜์œผ๋กœ ์„ค์ •
session_start([
    'cookie_secure' => true,
    'cookie_httponly' => true,
    'cookie_samesite' => 'Strict'
]);
?>

2. ์„ธ์…˜ ID ์žฌ์ƒ์„ฑ

๋กœ๊ทธ์ธ ์„ฑ๊ณต ํ›„์—๋Š” ๋ฐ˜๋“œ์‹œ ์„ธ์…˜ ID๋ฅผ ์žฌ์ƒ์„ฑํ•ด์•ผ ํ•ด. ์™œ๋ƒํ•˜๋ฉด ๋กœ๊ทธ์ธ ์ „์— ์‚ฌ์šฉํ•˜๋˜ ์„ธ์…˜ ID๋ฅผ ๊ทธ๋Œ€๋กœ ์“ฐ๋ฉด, ํ•ด์ปค๊ฐ€ ๋ฏธ๋ฆฌ ๊ทธ ID๋ฅผ ์•Œ๊ณ  ์žˆ์„ ์ˆ˜ ์žˆ๊ฑฐ๋“ . ์ด๊ฑธ ์„ธ์…˜ ๊ณ ์ • ๊ณต๊ฒฉ(Session Fixation)์ด๋ผ๊ณ  ํ•ด.

<?php
// ๋กœ๊ทธ์ธ ์ฒ˜๋ฆฌ ํ•จ์ˆ˜
function processLogin($username, $password) {
    // 1. ์‚ฌ์šฉ์ž ์ธ์ฆ
    $user = authenticateUser($username, $password);
    
    if($user) {
        // 2. ๊ธฐ์กด ์„ธ์…˜ ๋ฐ์ดํ„ฐ ๋ฐฑ์—…
        $oldSessionData = $_SESSION;
        
        // 3. ์„ธ์…˜ ID ์žฌ์ƒ์„ฑ (์ค‘์š”!)
        session_regenerate_id(true);
        
        // 4. ์ƒˆ๋กœ์šด ์„ธ์…˜์— ์‚ฌ์šฉ์ž ์ •๋ณด ์ €์žฅ
        $_SESSION['user_id'] = $user['id'];
        $_SESSION['username'] = $user['username'];
        $_SESSION['login_time'] = time();
        $_SESSION['ip_address'] = $_SERVER['REMOTE_ADDR'];
        $_SESSION['user_agent'] = $_SERVER['HTTP_USER_AGENT'];
        
        return true;
    }
    
    return false;
}
?>

3. ์„ธ์…˜ ๊ฒ€์ฆ ๊ฐ•ํ™”

๋งค ์š”์ฒญ๋งˆ๋‹ค ์„ธ์…˜์ด ์œ ํšจํ•œ์ง€ ๊ฒ€์ฆํ•˜๋Š” ๊ฒŒ ์ข‹์•„. IP ์ฃผ์†Œ๋‚˜ User-Agent๊ฐ€ ๊ฐ‘์ž๊ธฐ ๋ฐ”๋€Œ๋ฉด ์˜์‹ฌํ•ด๋ณผ ํ•„์š”๊ฐ€ ์žˆ์ง€. ๋ฌผ๋ก  ๋ชจ๋ฐ”์ผ ํ™˜๊ฒฝ์—์„œ๋Š” IP๊ฐ€ ์ž์ฃผ ๋ฐ”๋€” ์ˆ˜ ์žˆ์œผ๋‹ˆ๊นŒ, ๋„ˆ๋ฌด ์—„๊ฒฉํ•˜๊ฒŒ ํ•˜๋ฉด ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์ด ๋‚˜๋น ์งˆ ์ˆ˜ ์žˆ์–ด. ๊ท ํ˜•์ด ์ค‘์š”ํ•ด! โš–๏ธ

<?php
// ์„ธ์…˜ ๊ฒ€์ฆ ํ•จ์ˆ˜
function validateSession() {
    // ์„ธ์…˜์ด ์‹œ์ž‘๋˜์ง€ ์•Š์•˜์œผ๋ฉด false
    if(!isset($_SESSION['user_id'])) {
        return false;
    }
    
    // IP ์ฃผ์†Œ ๊ฒ€์ฆ (์„ ํƒ์ )
    if(isset($_SESSION['ip_address'])) {
        if($_SESSION['ip_address'] !== $_SERVER['REMOTE_ADDR']) {
            // IP๊ฐ€ ๋ณ€๊ฒฝ๋จ - ๋กœ๊ทธ ๊ธฐ๋ก ํ›„ ์„ธ์…˜ ์ข…๋ฃŒ
            logSecurityEvent('IP address changed');
            session_destroy();
            return false;
        }
    }
    
    // User-Agent ๊ฒ€์ฆ
    if(isset($_SESSION['user_agent'])) {
        if($_SESSION['user_agent'] !== $_SERVER['HTTP_USER_AGENT']) {
            logSecurityEvent('User agent changed');
            session_destroy();
            return false;
        }
    }
    
    // ์„ธ์…˜ ํƒ€์ž„์•„์›ƒ ๊ฒ€์ฆ (30๋ถ„)
    if(isset($_SESSION['last_activity'])) {
        if(time() - $_SESSION['last_activity'] > 1800) {
            session_destroy();
            return false;
        }
    }
    
    // ๋งˆ์ง€๋ง‰ ํ™œ๋™ ์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ
    $_SESSION['last_activity'] = time();
    
    return true;
}

// ๋ชจ๋“  ๋ณดํ˜ธ๋œ ํŽ˜์ด์ง€์—์„œ ์‚ฌ์šฉ
session_start();
if(!validateSession()) {
    header('Location: login.php');
    exit;
}
?>

๐Ÿช ์ฟ ํ‚ค ๋ณด์•ˆ ๊ฐ•ํ™” ์ „๋žต

์ฟ ํ‚ค๋„ ์ œ๋Œ€๋กœ ๋ณดํ˜ธํ•˜์ง€ ์•Š์œผ๋ฉด ์—ฌ๋Ÿฌ ๊ณต๊ฒฉ์— ์ทจ์•ฝํ•ด. ํŠนํžˆ XSS(Cross-Site Scripting) ๊ณต๊ฒฉ์œผ๋กœ ์ฟ ํ‚ค๋ฅผ ํƒˆ์ทจ๋‹นํ•  ์ˆ˜ ์žˆ์–ด. JavaScript๋กœ document.cookie๋ฅผ ์‹คํ–‰ํ•˜๋ฉด ์ฟ ํ‚ค ๊ฐ’์„ ์ฝ์„ ์ˆ˜ ์žˆ๊ฑฐ๋“ . ๐Ÿ˜จ

HttpOnly ํ”Œ๋ž˜๊ทธ

HttpOnly ํ”Œ๋ž˜๊ทธ๋ฅผ ์„ค์ •ํ•˜๋ฉด JavaScript์—์„œ ์ฟ ํ‚ค์— ์ ‘๊ทผํ•  ์ˆ˜ ์—†์–ด. ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด XSS ๊ณต๊ฒฉ์œผ๋กœ๋ถ€ํ„ฐ ์ฟ ํ‚ค๋ฅผ ๋ณดํ˜ธํ•  ์ˆ˜ ์žˆ์ง€. ์„ธ์…˜ ์ฟ ํ‚ค์—๋Š” ๋ฐ˜๋“œ์‹œ ์ด ์˜ต์…˜์„ ์ผœ์•ผ ํ•ด!

<?php
// HttpOnly ์ฟ ํ‚ค ์„ค์ •
setcookie(
    'session_token',
    $token,
    [
        'expires' => time() + 3600,
        'path' => '/',
        'domain' => '',
        'secure' => true,      // HTTPS์—์„œ๋งŒ
        'httponly' => true,    // JavaScript ์ ‘๊ทผ ์ฐจ๋‹จ
        'samesite' => 'Strict' // CSRF ๋ฐฉ์–ด
    ]
);
?>

SameSite ์†์„ฑ

SameSite ์†์„ฑ์€ CSRF(Cross-Site Request Forgery) ๊ณต๊ฒฉ์„ ๋ง‰์•„์ค˜. ์ด ์†์„ฑ์€ ์„ธ ๊ฐ€์ง€ ๊ฐ’์„ ๊ฐ€์งˆ ์ˆ˜ ์žˆ์–ด:

๐Ÿ“Œ SameSite ์˜ต์…˜ โ€ข Strict: ๊ฐ€์žฅ ์—„๊ฒฉ. ๊ฐ™์€ ์‚ฌ์ดํŠธ์—์„œ๋งŒ ์ฟ ํ‚ค ์ „์†ก. ์™ธ๋ถ€ ๋งํฌ๋กœ ์ ‘์†ํ•˜๋ฉด ์ฟ ํ‚ค๊ฐ€ ์•ˆ ๋ณด๋‚ด์ง
โ€ข Lax: ๊ธฐ๋ณธ๊ฐ’. ์ผ๋ฐ˜์ ์ธ GET ์š”์ฒญ์—๋Š” ์ฟ ํ‚ค ์ „์†ก. ๋Œ€๋ถ€๋ถ„์˜ ๊ฒฝ์šฐ ์ ์ ˆํ•จ
โ€ข None: ์ œํ•œ ์—†์Œ. ๋‹จ, Secure ์†์„ฑ๊ณผ ํ•จ๊ป˜ ์‚ฌ์šฉํ•ด์•ผ ํ•จ

์ฟ ํ‚ค ์•”ํ˜ธํ™”

๋ฏผ๊ฐํ•œ ์ •๋ณด๋ฅผ ์ฟ ํ‚ค์— ์ €์žฅํ•ด์•ผ ํ•œ๋‹ค๋ฉด (๊ถŒ์žฅํ•˜์ง€ ์•Š์ง€๋งŒ), ๋ฐ˜๋“œ์‹œ ์•”ํ˜ธํ™”ํ•ด์•ผ ํ•ด. PHP์˜ openssl ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์•ˆ์ „ํ•˜๊ฒŒ ์•”ํ˜ธํ™”ํ•  ์ˆ˜ ์žˆ์–ด.

<?php
// ์ฟ ํ‚ค ์•”ํ˜ธํ™” ํด๋ž˜์Šค
class SecureCookie {
    private $key;
    private $cipher = 'aes-256-cbc';
    
    public function __construct($key) {
        $this->key = $key;
    }
    
    // ์•”ํ˜ธํ™”
    public function encrypt($data) {
        $iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length($this->cipher));
        $encrypted = openssl_encrypt($data, $this->cipher, $this->key, 0, $iv);
        return base64_encode($encrypted . '::' . $iv);
    }
    
    // ๋ณตํ˜ธํ™”
    public function decrypt($data) {
        list($encrypted, $iv) = explode('::', base64_decode($data), 2);
        return openssl_decrypt($encrypted, $this->cipher, $this->key, 0, $iv);
    }
    
    // ์•ˆ์ „ํ•œ ์ฟ ํ‚ค ์„ค์ •
    public function setCookie($name, $value, $expire = 3600) {
        $encrypted = $this->encrypt($value);
        setcookie($name, $encrypted, [
            'expires' => time() + $expire,
            'path' => '/',
            'secure' => true,
            'httponly' => true,
            'samesite' => 'Strict'
        ]);
    }
    
    // ์ฟ ํ‚ค ์ฝ๊ธฐ
    public function getCookie($name) {
        if(isset($_COOKIE[$name])) {
            return $this->decrypt($_COOKIE[$name]);
        }
        return null;
    }
}

// ์‚ฌ์šฉ ์˜ˆ์‹œ
$key = 'your-secret-key-32-characters!!'; // ์‹ค์ œ๋กœ๋Š” ํ™˜๊ฒฝ๋ณ€์ˆ˜์—์„œ ๊ฐ€์ ธ์™€์•ผ ํ•จ
$secureCookie = new SecureCookie($key);

// ์ฟ ํ‚ค ์„ค์ •
$secureCookie->setCookie('user_data', json_encode([
    'user_id' => 123,
    'role' => 'user'
]));

// ์ฟ ํ‚ค ์ฝ๊ธฐ
$userData = json_decode($secureCookie->getCookie('user_data'), true);
?>
โš ๏ธ ์•”ํ˜ธํ™” ํ‚ค ๊ด€๋ฆฌ ์•”ํ˜ธํ™” ํ‚ค๋Š” ์ ˆ๋Œ€ ์ฝ”๋“œ์— ํ•˜๋“œ์ฝ”๋”ฉํ•˜๋ฉด ์•ˆ ๋ผ! ํ™˜๊ฒฝ๋ณ€์ˆ˜๋‚˜ ๋ณ„๋„์˜ ์„ค์ • ํŒŒ์ผ์— ์ €์žฅํ•˜๊ณ , ๊ทธ ํŒŒ์ผ์€ Git์— ์˜ฌ๋ฆฌ์ง€ ๋ง์•„์•ผ ํ•ด. ํ‚ค๊ฐ€ ๋…ธ์ถœ๋˜๋ฉด ์•”ํ˜ธํ™”๊ฐ€ ๋ฌด์šฉ์ง€๋ฌผ์ด ๋˜๊ฑฐ๋“ .

โฐ ์„ธ์…˜ ํƒ€์ž„์•„์›ƒ ๊ด€๋ฆฌ

์‚ฌ์šฉ์ž๊ฐ€ ๋กœ๊ทธ์ธํ•œ ์ฑ„๋กœ ์ž๋ฆฌ๋ฅผ ๋น„์› ๋Š”๋ฐ, ๋‹ค๋ฅธ ์‚ฌ๋žŒ์ด ๊ทธ ์ปดํ“จํ„ฐ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์–ด๋–ป๊ฒŒ ๋ ๊นŒ? ๐Ÿ˜ฑ ์ด๋Ÿฐ ์ƒํ™ฉ์„ ๋ง‰๊ธฐ ์œ„ํ•ด ์„ธ์…˜ ํƒ€์ž„์•„์›ƒ์„ ์„ค์ •ํ•ด์•ผ ํ•ด. ์ผ์ • ์‹œ๊ฐ„ ๋™์•ˆ ํ™œ๋™์ด ์—†์œผ๋ฉด ์ž๋™์œผ๋กœ ๋กœ๊ทธ์•„์›ƒ์‹œํ‚ค๋Š” ๊ฑฐ์ง€.

<?php
// ์„ธ์…˜ ํƒ€์ž„์•„์›ƒ ์„ค์ •
class SessionManager {
    private $timeout = 1800; // 30๋ถ„
    
    public function __construct($timeout = 1800) {
        $this->timeout = $timeout;
    }
    
    public function start() {
        session_start();
        
        // ์„ธ์…˜์ด ์ฒ˜์Œ ์‹œ์ž‘๋˜๋Š” ๊ฒฝ์šฐ
        if(!isset($_SESSION['created'])) {
            $_SESSION['created'] = time();
        }
        
        // ์„ธ์…˜ ์ƒ์„ฑ ํ›„ 2์‹œ๊ฐ„์ด ์ง€๋‚˜๋ฉด ์žฌ์ƒ์„ฑ (๋ณด์•ˆ)
        if(time() - $_SESSION['created'] > 7200) {
            session_regenerate_id(true);
            $_SESSION['created'] = time();
        }
        
        // ๋งˆ์ง€๋ง‰ ํ™œ๋™ ์‹œ๊ฐ„ ์ฒดํฌ
        if(isset($_SESSION['last_activity'])) {
            $inactive = time() - $_SESSION['last_activity'];
            
            if($inactive > $this->timeout) {
                // ํƒ€์ž„์•„์›ƒ ๋ฐœ์ƒ
                $this->destroy();
                return false;
            }
        }
        
        // ๋งˆ์ง€๋ง‰ ํ™œ๋™ ์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ
        $_SESSION['last_activity'] = time();
        return true;
    }
    
    public function destroy() {
        $_SESSION = array();
        
        // ์„ธ์…˜ ์ฟ ํ‚ค ์‚ญ์ œ
        if(isset($_COOKIE[session_name()])) {
            setcookie(session_name(), '', time() - 3600, '/');
        }
        
        session_destroy();
    }
    
    public function getRemainingTime() {
        if(isset($_SESSION['last_activity'])) {
            $elapsed = time() - $_SESSION['last_activity'];
            return max(0, $this->timeout - $elapsed);
        }
        return $this->timeout;
    }
}

// ์‚ฌ์šฉ ์˜ˆ์‹œ
$sessionManager = new SessionManager(1800); // 30๋ถ„ ํƒ€์ž„์•„์›ƒ

if(!$sessionManager->start()) {
    // ์„ธ์…˜ ํƒ€์ž„์•„์›ƒ ๋ฐœ์ƒ
    header('Location: login.php?timeout=1');
    exit;
}

// ๋‚จ์€ ์‹œ๊ฐ„ ํ‘œ์‹œ (JavaScript๋กœ ์นด์šดํŠธ๋‹ค์šด ๊ฐ€๋Šฅ)
$remainingTime = $sessionManager->getRemainingTime();
?>

Remember Me ๊ธฐ๋Šฅ ๊ตฌํ˜„

"๋กœ๊ทธ์ธ ์ƒํƒœ ์œ ์ง€" ์ฒดํฌ๋ฐ•์Šค ๋ณธ ์  ์žˆ์ง€? ์ด๊ฒŒ ๋ฐ”๋กœ Remember Me ๊ธฐ๋Šฅ์ด์•ผ. ์ด๊ฑธ ๊ตฌํ˜„ํ•  ๋•Œ๋Š” ์„ธ์…˜๋ณด๋‹ค ๊ธด ์ˆ˜๋ช…์„ ๊ฐ€์ง„ ์ฟ ํ‚ค๋ฅผ ์‚ฌ์šฉํ•ด. ํ•˜์ง€๋งŒ ๋ณด์•ˆ์— ๋” ์‹ ๊ฒฝ ์จ์•ผ ํ•ด!

<?php
class RememberMe {
    private $db;
    
    public function __construct($db) {
        $this->db = $db;
    }
    
    // Remember Me ํ† ํฐ ์ƒ์„ฑ
    public function createToken($userId) {
        // ์•ˆ์ „ํ•œ ๋žœ๋ค ํ† ํฐ ์ƒ์„ฑ
        $selector = bin2hex(random_bytes(16));
        $validator = bin2hex(random_bytes(32));
        
        // validator๋Š” ํ•ด์‹œํ•ด์„œ ์ €์žฅ
        $hashedValidator = hash('sha256', $validator);
        
        // DB์— ์ €์žฅ
        $stmt = $this->db->prepare("
            INSERT INTO remember_tokens (user_id, selector, hashed_validator, expires)
            VALUES (?, ?, ?, DATE_ADD(NOW(), INTERVAL 30 DAY))
        ");
        $stmt->execute([$userId, $selector, $hashedValidator]);
        
        // ์ฟ ํ‚ค์— selector:validator ํ˜•ํƒœ๋กœ ์ €์žฅ
        $token = $selector . ':' . $validator;
        setcookie('remember_token', $token, [
            'expires' => time() + (30 * 24 * 3600), // 30์ผ
            'path' => '/',
            'secure' => true,
            'httponly' => true,
            'samesite' => 'Strict'
        ]);
        
        return $token;
    }
    
    // Remember Me ํ† ํฐ ๊ฒ€์ฆ
    public function validateToken() {
        if(!isset($_COOKIE['remember_token'])) {
            return false;
        }
        
        $parts = explode(':', $_COOKIE['remember_token']);
        if(count($parts) !== 2) {
            return false;
        }
        
        list($selector, $validator) = $parts;
        
        // DB์—์„œ ํ† ํฐ ์กฐํšŒ
        $stmt = $this->db->prepare("
            SELECT user_id, hashed_validator, expires
            FROM remember_tokens
            WHERE selector = ? AND expires > NOW()
        ");
        $stmt->execute([$selector]);
        $token = $stmt->fetch();
        
        if(!$token) {
            return false;
        }
        
        // validator ๊ฒ€์ฆ
        if(hash('sha256', $validator) !== $token['hashed_validator']) {
            // ํ† ํฐ์ด ์ผ์น˜ํ•˜์ง€ ์•Š์Œ - ๊ณต๊ฒฉ ์‹œ๋„์ผ ์ˆ˜ ์žˆ์Œ
            $this->deleteAllTokens($token['user_id']);
            return false;
        }
        
        // ๊ฒ€์ฆ ์„ฑ๊ณต - ์„ธ์…˜ ์‹œ์ž‘
        session_regenerate_id(true);
        $_SESSION['user_id'] = $token['user_id'];
        
        // ๋ณด์•ˆ์„ ์œ„ํ•ด ํ† ํฐ ์žฌ์ƒ์„ฑ
        $this->deleteToken($selector);
        $this->createToken($token['user_id']);
        
        return $token['user_id'];
    }
    
    // ํ† ํฐ ์‚ญ์ œ
    public function deleteToken($selector) {
        $stmt = $this->db->prepare("DELETE FROM remember_tokens WHERE selector = ?");
        $stmt->execute([$selector]);
    }
    
    // ์‚ฌ์šฉ์ž์˜ ๋ชจ๋“  ํ† ํฐ ์‚ญ์ œ
    public function deleteAllTokens($userId) {
        $stmt = $this->db->prepare("DELETE FROM remember_tokens WHERE user_id = ?");
        $stmt->execute([$userId]);
    }
}

// ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ…Œ์ด๋ธ” ์ƒ์„ฑ SQL
/*
CREATE TABLE remember_tokens (
    id INT AUTO_INCREMENT PRIMARY KEY,
    user_id INT NOT NULL,
    selector VARCHAR(32) NOT NULL,
    hashed_validator VARCHAR(64) NOT NULL,
    expires DATETIME NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    INDEX(selector),
    INDEX(user_id)
);
*/
?>
๐Ÿ’ก Remember Me ๋ณด์•ˆ ํŒ Remember Me ํ† ํฐ์€ selector์™€ validator๋กœ ๋‚˜๋ˆ ์„œ ๊ด€๋ฆฌํ•ด์•ผ ํ•ด. Selector๋Š” DB์—์„œ ํ† ํฐ์„ ์ฐพ๋Š” ์šฉ๋„๊ณ , validator๋Š” ์‹ค์ œ ์ธ์ฆ์— ์‚ฌ์šฉ๋ผ. Validator๋Š” ํ•ด์‹œํ•ด์„œ ์ €์žฅํ•˜๊ธฐ ๋•Œ๋ฌธ์—, DB๊ฐ€ ํ•ดํ‚น๋‹นํ•ด๋„ ํ† ํฐ์„ ๋ณต๊ตฌํ•  ์ˆ˜ ์—†์–ด. ์ด๊ฒŒ ๋ฐ”๋กœ Split Token Pattern์ด์•ผ!

๐Ÿ”„ CSRF ๊ณต๊ฒฉ๊ณผ ๋ฐฉ์–ด

CSRF(Cross-Site Request Forgery)๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ์˜๋„ํ•˜์ง€ ์•Š์€ ์š”์ฒญ์„ ๋ณด๋‚ด๋„๋ก ๋งŒ๋“œ๋Š” ๊ณต๊ฒฉ์ด์•ผ. ์˜ˆ๋ฅผ ๋“ค์–ด, ์•…์„ฑ ์‚ฌ์ดํŠธ์— ์žˆ๋Š” ์ด๋ฏธ์ง€ ํƒœ๊ทธ๊ฐ€ ์‹ค์ œ๋กœ๋Š” ์€ํ–‰ ์†ก๊ธˆ ์š”์ฒญ์„ ๋ณด๋‚ด๋Š” ์‹์ด์ง€. ๐Ÿ˜ˆ

CSRF ๊ณต๊ฒฉ ์‹œ๋‚˜๋ฆฌ์˜ค 1. ์‚ฌ์šฉ์ž ๋กœ๊ทธ์ธ ์€ํ–‰ ์‚ฌ์ดํŠธ์— ์ •์ƒ ๋กœ๊ทธ์ธ 2. ์•…์„ฑ ์‚ฌ์ดํŠธ ๋ฐฉ๋ฌธ ์ˆจ๊ฒจ์ง„ ํผ์ด ์ž๋™ ์ œ์ถœ๋จ 3. ํ”ผํ•ด ๋ฐœ์ƒ ์‚ฌ์šฉ์ž ๋ชจ๋ฅด๊ฒŒ ์†ก๊ธˆ ์‹คํ–‰ CSRF ํ† ํฐ์œผ๋กœ ๋ฐฉ์–ดํ•˜๊ธฐ 1. ์„œ๋ฒ„๊ฐ€ ๊ฐ ํผ์— ๊ณ ์œ ํ•œ ํ† ํฐ ์ƒ์„ฑ 2. ์š”์ฒญ ์‹œ ํ† ํฐ ๊ฒ€์ฆ 3. ํ† ํฐ์ด ์—†๊ฑฐ๋‚˜ ํ‹€๋ฆฌ๋ฉด ์š”์ฒญ ๊ฑฐ๋ถ€

CSRF ํ† ํฐ ๊ตฌํ˜„

CSRF๋ฅผ ๋ง‰๋Š” ๊ฐ€์žฅ ํšจ๊ณผ์ ์ธ ๋ฐฉ๋ฒ•์€ CSRF ํ† ํฐ์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฑฐ์•ผ. ๊ฐ ํผ์— ๊ณ ์œ ํ•œ ํ† ํฐ์„ ๋„ฃ๊ณ , ์„œ๋ฒ„์—์„œ ๊ทธ ํ† ํฐ์„ ๊ฒ€์ฆํ•˜๋Š” ๋ฐฉ์‹์ด์ง€.

<?php
class CSRFProtection {
    private $tokenName = 'csrf_token';
    
    // CSRF ํ† ํฐ ์ƒ์„ฑ
    public function generateToken() {
        if(!isset($_SESSION[$this->tokenName])) {
            $_SESSION[$this->tokenName] = bin2hex(random_bytes(32));
        }
        return $_SESSION[$this->tokenName];
    }
    
    // CSRF ํ† ํฐ ๊ฒ€์ฆ
    public function validateToken($token) {
        if(!isset($_SESSION[$this->tokenName])) {
            return false;
        }
        
        // timing attack ๋ฐฉ์ง€๋ฅผ ์œ„ํ•ด hash_equals ์‚ฌ์šฉ
        return hash_equals($_SESSION[$this->tokenName], $token);
    }
    
    // HTML ํผ์— ํ† ํฐ ์‚ฝ์ž…
    public function getTokenField() {
        $token = $this->generateToken();
        return '<input type="hidden" name="' . $this->tokenName . '" value="' . htmlspecialchars($token) . '">';
    }
    
    // ํ† ํฐ ์žฌ์ƒ์„ฑ (์ผํšŒ์šฉ ํ† ํฐ)
    public function regenerateToken() {
        unset($_SESSION[$this->tokenName]);
        return $this->generateToken();
    }
}

// ์‚ฌ์šฉ ์˜ˆ์‹œ
session_start();
$csrf = new CSRFProtection();

// ํผ ์ƒ์„ฑ
?>
<form method="POST" action="transfer.php">
    <?php echo $csrf->getTokenField(); ?>
    <input type="text" name="amount" placeholder="๊ธˆ์•ก">
    <button type="submit">์†ก๊ธˆ</button>
</form>

<?php
// ํผ ์ฒ˜๋ฆฌ
if($_SERVER['REQUEST_METHOD'] === 'POST') {
    if(!$csrf->validateToken($_POST['csrf_token'] ?? '')) {
        die('CSRF ํ† ํฐ์ด ์œ ํšจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค!');
    }
    
    // ์ •์ƒ ์ฒ˜๋ฆฌ
    $amount = $_POST['amount'];
    // ... ์†ก๊ธˆ ๋กœ์ง
}
?>

AJAX ์š”์ฒญ์—์„œ์˜ CSRF ๋ฐฉ์–ด

์š”์ฆ˜์€ AJAX๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ์ฃผ๊ณ ๋ฐ›๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋งŽ์ž–์•„? AJAX ์š”์ฒญ์—์„œ๋„ CSRF ํ† ํฐ์„ ๋ณด๋‚ด์•ผ ํ•ด. ๋ณดํ†ต ์ปค์Šคํ…€ HTTP ํ—ค๋”์— ํ† ํฐ์„ ๋„ฃ์–ด์„œ ๋ณด๋‚ด๋Š” ๋ฐฉ์‹์„ ์‚ฌ์šฉํ•ด.

<?php
// API ์—”๋“œํฌ์ธํŠธ์—์„œ CSRF ๊ฒ€์ฆ
class APICSRFProtection {
    public function validateRequest() {
        $token = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
        
        if(!isset($_SESSION['csrf_token'])) {
            $this->sendError('CSRF token not found in session');
        }
        
        if(!hash_equals($_SESSION['csrf_token'], $token)) {
            $this->sendError('Invalid CSRF token');
        }
        
        return true;
    }
    
    private function sendError($message) {
        http_response_code(403);
        header('Content-Type: application/json');
        echo json_encode(['error' => $message]);
        exit;
    }
}
?>

<!-- JavaScript์—์„œ CSRF ํ† ํฐ ์‚ฌ์šฉ -->
<script>
// ํŽ˜์ด์ง€ ๋กœ๋“œ ์‹œ ํ† ํฐ ๊ฐ€์ ธ์˜ค๊ธฐ
const csrfToken = '<?php echo $_SESSION['csrf_token']; ?>';

// AJAX ์š”์ฒญ
fetch('/api/transfer', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'X-CSRF-Token': csrfToken
    },
    body: JSON.stringify({
        amount: 10000
    })
})
.then(response => response.json())
.then(data => console.log(data));
</script>

๐Ÿ—„๏ธ ์„ธ์…˜ ์Šคํ† ๋ฆฌ์ง€ ์ตœ์ ํ™”

ํŠธ๋ž˜ํ”ฝ์ด ๋งŽ์€ ์‚ฌ์ดํŠธ์—์„œ๋Š” ์„ธ์…˜ ์ €์žฅ ๋ฐฉ์‹์ด ์„ฑ๋Šฅ์— ํฐ ์˜ํ–ฅ์„ ๋ฏธ์ณ. ๊ธฐ๋ณธ ํŒŒ์ผ ์ €์žฅ ๋ฐฉ์‹์€ ๊ฐ„๋‹จํ•˜์ง€๋งŒ, ๋™์‹œ ์ ‘์†์ž๊ฐ€ ๋งŽ์œผ๋ฉด ๋””์Šคํฌ I/O๊ฐ€ ๋ณ‘๋ชฉ์ด ๋  ์ˆ˜ ์žˆ์–ด. ์žฌ๋Šฅ๋„ท์ฒ˜๋Ÿผ ๋งŽ์€ ์‚ฌ์šฉ์ž๊ฐ€ ๋™์‹œ์— ์ ‘์†ํ•˜๋Š” ํ”Œ๋žซํผ์—์„œ๋Š” ๋” ํšจ์œจ์ ์ธ ๋ฐฉ๋ฒ•์ด ํ•„์š”ํ•˜์ง€. ๐Ÿš€

Redis๋ฅผ ์ด์šฉํ•œ ์„ธ์…˜ ์ €์žฅ

Redis๋Š” ์ธ๋ฉ”๋ชจ๋ฆฌ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋กœ, ์—„์ฒญ๋‚˜๊ฒŒ ๋น ๋ฅธ ์ฝ๊ธฐ/์“ฐ๊ธฐ ์„ฑ๋Šฅ์„ ์ž๋ž‘ํ•ด. ์„ธ์…˜ ์ €์žฅ์†Œ๋กœ ์‚ฌ์šฉํ•˜๊ธฐ์— ์™„๋ฒฝํ•˜์ง€!

<?php
// Redis ์„ธ์…˜ ํ•ธ๋“ค๋Ÿฌ
class RedisSessionHandler implements SessionHandlerInterface {
    private $redis;
    private $ttl = 1800; // 30๋ถ„
    private $prefix = 'session:';
    
    public function __construct($host = '127.0.0.1', $port = 6379) {
        $this->redis = new Redis();
        $this->redis->connect($host, $port);
    }
    
    public function open($savePath, $sessionName) {
        return true;
    }
    
    public function close() {
        return true;
    }
    
    public function read($id) {
        $data = $this->redis->get($this->prefix . $id);
        return $data ? $data : '';
    }
    
    public function write($id, $data) {
        return $this->redis->setex(
            $this->prefix . $id,
            $this->ttl,
            $data
        );
    }
    
    public function destroy($id) {
        return $this->redis->del($this->prefix . $id) > 0;
    }
    
    public function gc($maxlifetime) {
        // Redis์˜ TTL์ด ์ž๋™์œผ๋กœ ์ฒ˜๋ฆฌํ•˜๋ฏ€๋กœ ๋ถˆํ•„์š”
        return true;
    }
}

// ์‚ฌ์šฉ ๋ฐฉ๋ฒ•
$handler = new RedisSessionHandler();
session_set_save_handler($handler, true);
session_start();
?>

๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ธ์…˜ ์ €์žฅ

Redis๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†๋Š” ํ™˜๊ฒฝ์ด๋ผ๋ฉด ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜๋„ ์žˆ์–ด. ํŒŒ์ผ๋ณด๋‹ค๋Š” ๋А๋ฆฌ์ง€๋งŒ, ์—ฌ๋Ÿฌ ์„œ๋ฒ„์—์„œ ์„ธ์…˜์„ ๊ณต์œ ํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ์žฅ์ ์ด ์žˆ์ง€.

<?php
class DatabaseSessionHandler implements SessionHandlerInterface {
    private $db;
    
    public function __construct($db) {
        $this->db = $db;
    }
    
    public function open($savePath, $sessionName) {
        return true;
    }
    
    public function close() {
        return true;
    }
    
    public function read($id) {
        $stmt = $this->db->prepare("
            SELECT data FROM sessions 
            WHERE id = ? AND expires > NOW()
        ");
        $stmt->execute([$id]);
        $result = $stmt->fetch();
        
        return $result ? $result['data'] : '';
    }
    
    public function write($id, $data) {
        $stmt = $this->db->prepare("
            INSERT INTO sessions (id, data, expires) 
            VALUES (?, ?, DATE_ADD(NOW(), INTERVAL 30 MINUTE))
            ON DUPLICATE KEY UPDATE 
                data = VALUES(data),
                expires = VALUES(expires)
        ");
        return $stmt->execute([$id, $data]);
    }
    
    public function destroy($id) {
        $stmt = $this->db->prepare("DELETE FROM sessions WHERE id = ?");
        return $stmt->execute([$id]);
    }
    
    public function gc($maxlifetime) {
        $stmt = $this->db->prepare("DELETE FROM sessions WHERE expires < NOW()");
        return $stmt->execute();
    }
}

// ํ…Œ์ด๋ธ” ์ƒ์„ฑ SQL
/*
CREATE TABLE sessions (
    id VARCHAR(128) PRIMARY KEY,
    data TEXT,
    expires DATETIME,
    INDEX(expires)
);
*/
?>
์ €์žฅ ๋ฐฉ์‹ ์„ฑ๋Šฅ ํ™•์žฅ์„ฑ ๋น„์šฉ ์ถ”์ฒœ ๊ทœ๋ชจ
ํŒŒ์ผ โญโญ โญ ๋ฌด๋ฃŒ ์†Œ๊ทœ๋ชจ (~1,000๋ช…)
๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค โญโญโญ โญโญโญ ์ €๋ ด ์ค‘๊ทœ๋ชจ (~10,000๋ช…)
Redis โญโญโญโญโญ โญโญโญโญโญ ์ค‘๊ฐ„ ๋Œ€๊ทœ๋ชจ (๋ฌด์ œํ•œ)
Memcached โญโญโญโญโญ โญโญโญโญ ์ค‘๊ฐ„ ๋Œ€๊ทœ๋ชจ (๋ฌด์ œํ•œ)

๐ŸŽฏ ์‹ค์ „ ์˜ˆ์ œ: ์™„์ „ํ•œ ๋กœ๊ทธ์ธ ์‹œ์Šคํ…œ

์ž, ์ด์ œ ์ง€๊ธˆ๊นŒ์ง€ ๋ฐฐ์šด ๊ฑธ ๋ชจ๋‘ ํ•ฉ์ณ์„œ ์•ˆ์ „ํ•œ ๋กœ๊ทธ์ธ ์‹œ์Šคํ…œ์„ ๋งŒ๋“ค์–ด๋ณด์ž! ์ด๊ฑด ์‹ค์ œ ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ์ˆ˜์ค€์ด์•ผ. ๐Ÿ’ช

<?php
// config.php - ์„ค์ • ํŒŒ์ผ
define('DB_HOST', 'localhost');
define('DB_NAME', 'myapp');
define('DB_USER', 'root');
define('DB_PASS', 'password');
define('ENCRYPTION_KEY', 'your-32-character-secret-key!');

// ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ
try {
    $pdo = new PDO(
        "mysql:host=" . DB_HOST . ";dbname=" . DB_NAME . ";charset=utf8mb4",
        DB_USER,
        DB_PASS,
        [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
            PDO::ATTR_EMULATE_PREPARES => false
        ]
    );
} catch(PDOException $e) {
    die('Database connection failed: ' . $e->getMessage());
}

// Auth.php - ์ธ์ฆ ํด๋ž˜์Šค
class Auth {
    private $db;
    private $csrf;
    private $rememberMe;
    
    public function __construct($db) {
        $this->db = $db;
        $this->csrf = new CSRFProtection();
        $this->rememberMe = new RememberMe($db);
        
        // ์„ธ์…˜ ๋ณด์•ˆ ์„ค์ •
        ini_set('session.cookie_httponly', 1);
        ini_set('session.cookie_secure', 1);
        ini_set('session.cookie_samesite', 'Strict');
        ini_set('session.use_strict_mode', 1);
        
        session_start();
        
        // Remember Me ํ† ํฐ ํ™•์ธ
        if(!$this->isLoggedIn() && isset($_COOKIE['remember_token'])) {
            $userId = $this->rememberMe->validateToken();
            if($userId) {
                $_SESSION['user_id'] = $userId;
                $this->loadUserData($userId);
            }
        }
    }
    
    // ํšŒ์›๊ฐ€์ž…
    public function register($username, $email, $password) {
        // ์ž…๋ ฅ ๊ฒ€์ฆ
        if(strlen($password) < 8) {
            return ['success' => false, 'message' => '๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” 8์ž ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.'];
        }
        
        // ์ด๋ฉ”์ผ ์ค‘๋ณต ํ™•์ธ
        $stmt = $this->db->prepare("SELECT id FROM users WHERE email = ?");
        $stmt->execute([$email]);
        if($stmt->fetch()) {
            return ['success' => false, 'message' => '์ด๋ฏธ ์‚ฌ์šฉ ์ค‘์ธ ์ด๋ฉ”์ผ์ž…๋‹ˆ๋‹ค.'];
        }
        
        // ๋น„๋ฐ€๋ฒˆํ˜ธ ํ•ด์‹œ
        $hashedPassword = password_hash($password, PASSWORD_ARGON2ID, [
            'memory_cost' => 65536,
            'time_cost' => 4,
            'threads' => 3
        ]);
        
        // ์‚ฌ์šฉ์ž ์ƒ์„ฑ
        $stmt = $this->db->prepare("
            INSERT INTO users (username, email, password, created_at)
            VALUES (?, ?, ?, NOW())
        ");
        
        try {
            $stmt->execute([$username, $email, $hashedPassword]);
            return ['success' => true, 'message' => 'ํšŒ์›๊ฐ€์ž…์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.'];
        } catch(PDOException $e) {
            return ['success' => false, 'message' => 'ํšŒ์›๊ฐ€์ž… ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.'];
        }
    }
    
    // ๋กœ๊ทธ์ธ
    public function login($email, $password, $rememberMe = false) {
        // ์‚ฌ์šฉ์ž ์กฐํšŒ
        $stmt = $this->db->prepare("
            SELECT id, username, password, failed_attempts, locked_until
            FROM users WHERE email = ?
        ");
        $stmt->execute([$email]);
        $user = $stmt->fetch();
        
        if(!$user) {
            $this->logFailedAttempt($email);
            return ['success' => false, 'message' => '์ด๋ฉ”์ผ ๋˜๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค.'];
        }
        
        // ๊ณ„์ • ์ž ๊ธˆ ํ™•์ธ
        if($user['locked_until'] && strtotime($user['locked_until']) > time()) {
            return ['success' => false, 'message' => '๊ณ„์ •์ด ์ผ์‹œ์ ์œผ๋กœ ์ž ๊ฒผ์Šต๋‹ˆ๋‹ค. ๋‚˜์ค‘์— ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.'];
        }
        
        // ๋น„๋ฐ€๋ฒˆํ˜ธ ํ™•์ธ
        if(!password_verify($password, $user['password'])) {
            $this->logFailedAttempt($email);
            return ['success' => false, 'message' => '์ด๋ฉ”์ผ ๋˜๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค.'];
        }
        
        // ๋น„๋ฐ€๋ฒˆํ˜ธ ํ•ด์‹œ ์—…๊ทธ๋ ˆ์ด๋“œ ํ•„์š” ์‹œ
        if(password_needs_rehash($user['password'], PASSWORD_ARGON2ID)) {
            $newHash = password_hash($password, PASSWORD_ARGON2ID);
            $stmt = $this->db->prepare("UPDATE users SET password = ? WHERE id = ?");
            $stmt->execute([$newHash, $user['id']]);
        }
        
        // ๋กœ๊ทธ์ธ ์„ฑ๊ณต - ์„ธ์…˜ ์„ค์ •
        session_regenerate_id(true);
        $_SESSION['user_id'] = $user['id'];
        $_SESSION['username'] = $user['username'];
        $_SESSION['login_time'] = time();
        $_SESSION['ip_address'] = $_SERVER['REMOTE_ADDR'];
        $_SESSION['user_agent'] = $_SERVER['HTTP_USER_AGENT'];
        
        // ์‹คํŒจ ํšŸ์ˆ˜ ์ดˆ๊ธฐํ™”
        $stmt = $this->db->prepare("
            UPDATE users 
            SET failed_attempts = 0, locked_until = NULL, last_login = NOW()
            WHERE id = ?
        ");
        $stmt->execute([$user['id']]);
        
        // Remember Me ์ฒ˜๋ฆฌ
        if($rememberMe) {
            $this->rememberMe->createToken($user['id']);
        }
        
        return ['success' => true, 'message' => '๋กœ๊ทธ์ธ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.'];
    }
    
    // ๋กœ๊ทธ์•„์›ƒ
    public function logout() {
        // Remember Me ํ† ํฐ ์‚ญ์ œ
        if(isset($_COOKIE['remember_token'])) {
            $parts = explode(':', $_COOKIE['remember_token']);
            if(count($parts) === 2) {
                $this->rememberMe->deleteToken($parts[0]);
            }
            setcookie('remember_token', '', time() - 3600, '/');
        }
        
        // ์„ธ์…˜ ์‚ญ์ œ
        $_SESSION = array();
        if(isset($_COOKIE[session_name()])) {
            setcookie(session_name(), '', time() - 3600, '/');
        }
        session_destroy();
    }
    
    // ๋กœ๊ทธ์ธ ์ƒํƒœ ํ™•์ธ
    public function isLoggedIn() {
        if(!isset($_SESSION['user_id'])) {
            return false;
        }
        
        // ์„ธ์…˜ ํƒ€์ž„์•„์›ƒ ํ™•์ธ (30๋ถ„)
        if(isset($_SESSION['last_activity'])) {
            if(time() - $_SESSION['last_activity'] > 1800) {
                $this->logout();
                return false;
            }
        }
        
        $_SESSION['last_activity'] = time();
        return true;
    }
    
    // ์‹คํŒจ ์‹œ๋„ ๊ธฐ๋ก
    private function logFailedAttempt($email) {
        $stmt = $this->db->prepare("
            UPDATE users 
            SET failed_attempts = failed_attempts + 1,
                locked_until = CASE 
                    WHEN failed_attempts >= 4 THEN DATE_ADD(NOW(), INTERVAL 15 MINUTE)
                    ELSE locked_until
                END
            WHERE email = ?
        ");
        $stmt->execute([$email]);
    }
    
    // ์‚ฌ์šฉ์ž ๋ฐ์ดํ„ฐ ๋กœ๋“œ
    private function loadUserData($userId) {
        $stmt = $this->db->prepare("SELECT username FROM users WHERE id = ?");
        $stmt->execute([$userId]);
        $user = $stmt->fetch();
        
        if($user) {
            $_SESSION['username'] = $user['username'];
        }
    }
    
    // CSRF ํ† ํฐ ๊ฐ€์ ธ์˜ค๊ธฐ
    public function getCsrfToken() {
        return $this->csrf->generateToken();
    }
    
    // CSRF ํ† ํฐ ๊ฒ€์ฆ
    public function validateCsrfToken($token) {
        return $this->csrf->validateToken($token);
    }
}
?>

๋กœ๊ทธ์ธ ํผ (login.php)

<?php
require_once 'config.php';
require_once 'Auth.php';

$auth = new Auth($pdo);

// ์ด๋ฏธ ๋กœ๊ทธ์ธ๋œ ๊ฒฝ์šฐ
if($auth->isLoggedIn()) {
    header('Location: dashboard.php');
    exit;
}

$error = '';

// ํผ ์ œ์ถœ ์ฒ˜๋ฆฌ
if($_SERVER['REQUEST_METHOD'] === 'POST') {
    if(!$auth->validateCsrfToken($_POST['csrf_token'] ?? '')) {
        $error = 'CSRF ํ† ํฐ์ด ์œ ํšจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.';
    } else {
        $email = $_POST['email'] ?? '';
        $password = $_POST['password'] ?? '';
        $rememberMe = isset($_POST['remember_me']);
        
        $result = $auth->login($email, $password, $rememberMe);
        
        if($result['success']) {
            header('Location: dashboard.php');
            exit;
        } else {
            $error = $result['message'];
        }
    }
}
?>
<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>๋กœ๊ทธ์ธ</title>
    <style>
        body { font-family: Arial, sans-serif; max-width: 400px; margin: 50px auto; padding: 20px; }
        .form-group { margin-bottom: 15px; }
        label { display: block; margin-bottom: 5px; font-weight: bold; }
        input[type="email"], input[type="password"] { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; }
        button { width: 100%; padding: 12px; background: #667eea; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; }
        button:hover { background: #5568d3; }
        .error { background: #fee; color: #c33; padding: 10px; border-radius: 4px; margin-bottom: 15px; }
        .checkbox-group { display: flex; align-items: center; }
        .checkbox-group input { width: auto; margin-right: 8px; }
    </style>
</head>
<body>
    <h1>๋กœ๊ทธ์ธ</h1>
    
    <?php if($error): ?>
        <div class="error"><?php echo htmlspecialchars($error); ?></div>
    <?php endif; ?>
    
    <form method="POST">
        <input type="hidden" name="csrf_token" value="<?php echo $auth->getCsrfToken(); ?>">
        
        <div class="form-group">
            <label>์ด๋ฉ”์ผ</label>
            <input type="email" name="email" required>
        </div>
        
        <div class="form-group">
            <label>๋น„๋ฐ€๋ฒˆํ˜ธ</label>
            <input type="password" name="password" required>
        </div>
        
        <div class="form-group checkbox-group">
            <input type="checkbox" name="remember_me" id="remember_me">
            <label for="remember_me" style="margin: 0;">๋กœ๊ทธ์ธ ์ƒํƒœ ์œ ์ง€</label>
        </div>
        
        <button type="submit">๋กœ๊ทธ์ธ</button>
    </form>
    
    <p style="text-align: center; margin-top: 20px;">
        ๊ณ„์ •์ด ์—†์œผ์‹ ๊ฐ€์š”? <a href="register.php">ํšŒ์›๊ฐ€์ž…</a>
    </p>
</body>
</html>
๐Ÿ’ก ๋ณด์•ˆ ์ฒดํฌ๋ฆฌ์ŠคํŠธ
  • โœ… HTTPS ์‚ฌ์šฉ
  • โœ… ๋น„๋ฐ€๋ฒˆํ˜ธ ํ•ด์‹œ (Argon2id ๋˜๋Š” bcrypt)
  • โœ… CSRF ํ† ํฐ ๊ฒ€์ฆ
  • โœ… ์„ธ์…˜ ID ์žฌ์ƒ์„ฑ
  • โœ… HttpOnly, Secure, SameSite ์ฟ ํ‚ค ํ”Œ๋ž˜๊ทธ
  • โœ… ๋กœ๊ทธ์ธ ์‹คํŒจ ํšŸ์ˆ˜ ์ œํ•œ
  • โœ… ์„ธ์…˜ ํƒ€์ž„์•„์›ƒ
  • โœ… Remember Me ํ† ํฐ ์•”ํ˜ธํ™”
  • โœ… SQL Injection ๋ฐฉ์ง€ (Prepared Statements)
  • โœ… XSS ๋ฐฉ์ง€ (htmlspecialchars)

๐Ÿ“Š ์„ธ์…˜ ๋ชจ๋‹ˆํ„ฐ๋ง๊ณผ ๊ด€๋ฆฌ

์šด์˜ ์ค‘์ธ ์„œ๋น„์Šค์—์„œ๋Š” ์„ธ์…˜์„ ๋ชจ๋‹ˆํ„ฐ๋งํ•˜๊ณ  ๊ด€๋ฆฌํ•˜๋Š” ๊ฒŒ ์ค‘์š”ํ•ด. ๋™์‹œ ์ ‘์†์ž ์ˆ˜๋ฅผ ํŒŒ์•…ํ•˜๊ณ , ์ด์ƒํ•œ ํ™œ๋™์„ ๊ฐ์ง€ํ•˜๊ณ , ํ•„์š”ํ•˜๋ฉด ํŠน์ • ์„ธ์…˜์„ ๊ฐ•์ œ๋กœ ์ข…๋ฃŒํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•˜๊ฑฐ๋“ . ๐Ÿ”

<?php
// SessionMonitor.php - ์„ธ์…˜ ๋ชจ๋‹ˆํ„ฐ๋ง ํด๋ž˜์Šค
class SessionMonitor {
    private $db;
    
    public function __construct($db) {
        $this->db = $db;
    }
    
    // ํ™œ์„ฑ ์„ธ์…˜ ์ˆ˜ ์กฐํšŒ
    public function getActiveSessionCount() {
        $stmt = $this->db->query("
            SELECT COUNT(*) as count 
            FROM sessions 
            WHERE expires > NOW()
        ");
        return $stmt->fetch()['count'];
    }
    
    // ์‚ฌ์šฉ์ž๋ณ„ ์„ธ์…˜ ๋ชฉ๋ก
    public function getUserSessions($userId) {
        $stmt = $this->db->prepare("
            SELECT 
                id,
                ip_address,
                user_agent,
                last_activity,
                created_at
            FROM user_sessions
            WHERE user_id = ? AND expires > NOW()
            ORDER BY last_activity DESC
        ");
        $stmt->execute([$userId]);
        return $stmt->fetchAll();
    }
    
    // ํŠน์ • ์„ธ์…˜ ๊ฐ•์ œ ์ข…๋ฃŒ
    public function terminateSession($sessionId) {
        $stmt = $this->db->prepare("DELETE FROM sessions WHERE id = ?");
        return $stmt->execute([$sessionId]);
    }
    
    // ์‚ฌ์šฉ์ž์˜ ๋ชจ๋“  ์„ธ์…˜ ์ข…๋ฃŒ (๋‹ค๋ฅธ ๊ธฐ๊ธฐ์—์„œ ๋กœ๊ทธ์•„์›ƒ)
    public function terminateAllUserSessions($userId, $exceptCurrent = true) {
        $currentSessionId = session_id();
        
        if($exceptCurrent) {
            $stmt = $this->db->prepare("
                DELETE FROM user_sessions 
                WHERE user_id = ? AND session_id != ?
            ");
            $stmt->execute([$userId, $currentSessionId]);
        } else {
            $stmt = $this->db->prepare("
                DELETE FROM user_sessions 
                WHERE user_id = ?
            ");
            $stmt->execute([$userId]);
        }
    }
    
    // ์˜์‹ฌ์Šค๋Ÿฌ์šด ํ™œ๋™ ๊ฐ์ง€
    public function detectSuspiciousActivity($userId) {
        // ์งง์€ ์‹œ๊ฐ„์— ์—ฌ๋Ÿฌ IP์—์„œ ์ ‘์†
        $stmt = $this->db->prepare("
            SELECT COUNT(DISTINCT ip_address) as ip_count
            FROM user_sessions
            WHERE user_id = ? 
            AND last_activity > DATE_SUB(NOW(), INTERVAL 5 MINUTE)
        ");
        $stmt->execute([$userId]);
        $result = $stmt->fetch();
        
        if($result['ip_count'] > 3) {
            return [
                'suspicious' => true,
                'reason' => '์งง์€ ์‹œ๊ฐ„์— ์—ฌ๋Ÿฌ IP์—์„œ ์ ‘์† ๊ฐ์ง€',
                'ip_count' => $result['ip_count']
            ];
        }
        
        return ['suspicious' => false];
    }
    
    // ์„ธ์…˜ ํ†ต๊ณ„
    public function getSessionStats() {
        $stats = [];
        
        // ์ด ํ™œ์„ฑ ์„ธ์…˜
        $stmt = $this->db->query("
            SELECT COUNT(*) as count 
            FROM sessions 
            WHERE expires > NOW()
        ");
        $stats['active_sessions'] = $stmt->fetch()['count'];
        
        // ์˜ค๋Š˜ ๋กœ๊ทธ์ธํ•œ ์‚ฌ์šฉ์ž ์ˆ˜
        $stmt = $this->db->query("
            SELECT COUNT(DISTINCT user_id) as count
            FROM user_sessions
            WHERE created_at > CURDATE()
        ");
        $stats['daily_logins'] = $stmt->fetch()['count'];
        
        // ํ‰๊ท  ์„ธ์…˜ ์‹œ๊ฐ„
        $stmt = $this->db->query("
            SELECT AVG(TIMESTAMPDIFF(MINUTE, created_at, last_activity)) as avg_duration
            FROM user_sessions
            WHERE last_activity > DATE_SUB(NOW(), INTERVAL 1 DAY)
        ");
        $stats['avg_session_duration'] = round($stmt->fetch()['avg_duration'], 2);
        
        return $stats;
    }
}

// ์‚ฌ์šฉ ์˜ˆ์‹œ
$monitor = new SessionMonitor($pdo);

// ๋Œ€์‹œ๋ณด๋“œ์—์„œ ํ†ต๊ณ„ ํ‘œ์‹œ
$stats = $monitor->getSessionStats();
echo "ํ˜„์žฌ ์ ‘์†์ž: " . $stats['active_sessions'] . "๋ช…\n";
echo "์˜ค๋Š˜ ๋กœ๊ทธ์ธ: " . $stats['daily_logins'] . "๋ช…\n";
echo "ํ‰๊ท  ์„ธ์…˜ ์‹œ๊ฐ„: " . $stats['avg_session_duration'] . "๋ถ„\n";

// ์‚ฌ์šฉ์ž์˜ ๋‹ค๋ฅธ ๊ธฐ๊ธฐ ์„ธ์…˜ ํ™•์ธ
$sessions = $monitor->getUserSessions($_SESSION['user_id']);
foreach($sessions as $session) {
    echo "IP: {$session['ip_address']}, ";
    echo "๋งˆ์ง€๋ง‰ ํ™œ๋™: {$session['last_activity']}\n";
}

// ์˜์‹ฌ์Šค๋Ÿฌ์šด ํ™œ๋™ ๊ฐ์ง€
$suspicious = $monitor->detectSuspiciousActivity($_SESSION['user_id']);
if($suspicious['suspicious']) {
    // ์•Œ๋ฆผ ๋ฐœ์†ก ๋˜๋Š” ์ถ”๊ฐ€ ์ธ์ฆ ์š”๊ตฌ
    sendSecurityAlert($_SESSION['user_id'], $suspicious['reason']);
}
?>

๐ŸŒ ๋‹ค์ค‘ ์„œ๋ฒ„ ํ™˜๊ฒฝ์—์„œ์˜ ์„ธ์…˜ ๊ด€๋ฆฌ

์„œ๋น„์Šค๊ฐ€ ์ปค์ง€๋ฉด ์„œ๋ฒ„๋ฅผ ์—ฌ๋Ÿฌ ๋Œ€ ์šด์˜ํ•˜๊ฒŒ ๋ผ. ์ด๋•Œ ๋ฌธ์ œ๊ฐ€ ์ƒ๊ธฐ๋Š”๋ฐ, ์‚ฌ์šฉ์ž๊ฐ€ A ์„œ๋ฒ„์—์„œ ๋กœ๊ทธ์ธํ–ˆ๋Š”๋ฐ ๋‹ค์Œ ์š”์ฒญ์ด B ์„œ๋ฒ„๋กœ ๊ฐ€๋ฉด ์„ธ์…˜์„ ์ฐพ์„ ์ˆ˜ ์—†๊ฒŒ ๋˜๋Š” ๊ฑฐ์•ผ. ์ด๊ฑธ ํ•ด๊ฒฐํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์•Œ์•„๋ณด์ž! ๐Ÿ–ฅ๏ธ๐Ÿ–ฅ๏ธ๐Ÿ–ฅ๏ธ

1. Sticky Session (์„ธ์…˜ ๊ณ ์ •)

๋กœ๋“œ ๋ฐธ๋Ÿฐ์„œ๊ฐ€ ๊ฐ™์€ ์‚ฌ์šฉ์ž์˜ ์š”์ฒญ์„ ํ•ญ์ƒ ๊ฐ™์€ ์„œ๋ฒ„๋กœ ๋ณด๋‚ด๋Š” ๋ฐฉ์‹์ด์•ผ. ๊ฐ„๋‹จํ•˜์ง€๋งŒ, ์„œ๋ฒ„ ํ•œ ๋Œ€๊ฐ€ ์ฃฝ์œผ๋ฉด ๊ทธ ์„œ๋ฒ„์˜ ์„ธ์…˜์ด ๋ชจ๋‘ ์‚ฌ๋ผ์ง€๋Š” ๋‹จ์ ์ด ์žˆ์–ด.

2. ์ค‘์•™ ์ง‘์ค‘์‹ ์„ธ์…˜ ์ €์žฅ์†Œ

Redis๋‚˜ Memcached ๊ฐ™์€ ์ค‘์•™ ์ €์žฅ์†Œ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋ชจ๋“  ์„œ๋ฒ„๊ฐ€ ๊ฐ™์€ ์„ธ์…˜ ๋ฐ์ดํ„ฐ๋ฅผ ๊ณต์œ ํ•  ์ˆ˜ ์žˆ์–ด. ์ด๊ฒŒ ๊ฐ€์žฅ ๊ถŒ์žฅ๋˜๋Š” ๋ฐฉ์‹์ด์•ผ!

<?php
// ์—ฌ๋Ÿฌ ์„œ๋ฒ„์—์„œ Redis ์„ธ์…˜ ๊ณต์œ 
// ์„œ๋ฒ„ 1, 2, 3 ๋ชจ๋‘ ๊ฐ™์€ ์„ค์ • ์‚ฌ์šฉ

// php.ini ๋˜๋Š” ์ฝ”๋“œ์—์„œ ์„ค์ •
ini_set('session.save_handler', 'redis');
ini_set('session.save_path', 'tcp://redis-server:6379?auth=password');

// ๋˜๋Š” ์ปค์Šคํ…€ ํ•ธ๋“ค๋Ÿฌ ์‚ฌ์šฉ
$redis = new Redis();
$redis->connect('redis-server', 6379);
$redis->auth('password');

$handler = new RedisSessionHandler($redis);
session_set_save_handler($handler, true);
session_start();

// ์ด์ œ ์–ด๋А ์„œ๋ฒ„์—์„œ๋“  ๊ฐ™์€ ์„ธ์…˜ ๋ฐ์ดํ„ฐ์— ์ ‘๊ทผ ๊ฐ€๋Šฅ!
?>

3. JWT (JSON Web Token)

์•„์˜ˆ ์„œ๋ฒ„์— ์„ธ์…˜์„ ์ €์žฅํ•˜์ง€ ์•Š๊ณ , ๋ชจ๋“  ์ •๋ณด๋ฅผ ์•”ํ˜ธํ™”๋œ ํ† ํฐ์— ๋‹ด์•„์„œ ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ์ฃผ๋Š” ๋ฐฉ์‹๋„ ์žˆ์–ด. ์ด๊ฑธ Stateless ๋ฐฉ์‹์ด๋ผ๊ณ  ํ•ด. API ์„œ๋ฒ„์—์„œ ๋งŽ์ด ์‚ฌ์šฉํ•˜์ง€.

<?php
// JWT ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์‚ฌ์šฉ (composer require firebase/php-jwt)
use Firebase\JWT\JWT;
use Firebase\JWT\Key;

class JWTAuth {
    private $secretKey = 'your-secret-key-keep-it-safe';
    private $algorithm = 'HS256';
    
    // JWT ํ† ํฐ ์ƒ์„ฑ
    public function createToken($userId, $username) {
        $issuedAt = time();
        $expire = $issuedAt + 3600; // 1์‹œ๊ฐ„
        
        $payload = [
            'iat' => $issuedAt,
            'exp' => $expire,
            'user_id' => $userId,
            'username' => $username
        ];
        
        return JWT::encode($payload, $this->secretKey, $this->algorithm);
    }
    
    // JWT ํ† ํฐ ๊ฒ€์ฆ
    public function validateToken($token) {
        try {
            $decoded = JWT::decode($token, new Key($this->secretKey, $this->algorithm));
            return [
                'valid' => true,
                'user_id' => $decoded->user_id,
                'username' => $decoded->username
            ];
        } catch(Exception $e) {
            return ['valid' => false, 'error' => $e->getMessage()];
        }
    }
    
    // ํ† ํฐ ๊ฐฑ์‹ 
    public function refreshToken($oldToken) {
        $result = $this->validateToken($oldToken);
        
        if($result['valid']) {
            return $this->createToken($result['user_id'], $result['username']);
        }
        
        return false;
    }
}

// ์‚ฌ์šฉ ์˜ˆ์‹œ
$jwtAuth = new JWTAuth();

// ๋กœ๊ทธ์ธ ์‹œ ํ† ํฐ ๋ฐœ๊ธ‰
$token = $jwtAuth->createToken(123, 'ํ™๊ธธ๋™');
echo "ํ† ํฐ: " . $token . "\n";

// API ์š”์ฒญ ์‹œ ํ† ํฐ ๊ฒ€์ฆ
$authHeader = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
if(preg_match('/Bearer\s+(.*)$/i', $authHeader, $matches)) {
    $token = $matches[1];
    $result = $jwtAuth->validateToken($token);
    
    if($result['valid']) {
        echo "์ธ์ฆ ์„ฑ๊ณต: " . $result['username'];
    } else {
        http_response_code(401);
        echo "์ธ์ฆ ์‹คํŒจ: " . $result['error'];
    }
}
?>
๐Ÿ“Œ JWT vs ์„ธ์…˜ ๋น„๊ต JWT ์žฅ์ :
โ€ข ์„œ๋ฒ„์— ์ƒํƒœ ์ €์žฅ ๋ถˆํ•„์š” (ํ™•์žฅ์„ฑ ์šฐ์ˆ˜)
โ€ข ์—ฌ๋Ÿฌ ๋„๋ฉ”์ธ์—์„œ ์‚ฌ์šฉ ๊ฐ€๋Šฅ
โ€ข ๋ชจ๋ฐ”์ผ ์•ฑ์— ์ ํ•ฉ

JWT ๋‹จ์ :
โ€ข ํ† ํฐ ํฌ๊ธฐ๊ฐ€ ํผ (๋งค ์š”์ฒญ๋งˆ๋‹ค ์ „์†ก)
โ€ข ํ† ํฐ ๋ฌดํšจํ™”๊ฐ€ ์–ด๋ ค์›€
โ€ข ๋ฏผ๊ฐํ•œ ์ •๋ณด ์ €์žฅ ๋ถˆ๊ฐ€

์„ธ์…˜ ์žฅ์ :
โ€ข ์„œ๋ฒ„์—์„œ ์™„์ „ํ•œ ์ œ์–ด ๊ฐ€๋Šฅ
โ€ข ์ฆ‰์‹œ ๋ฌดํšจํ™” ๊ฐ€๋Šฅ
โ€ข ๋” ๋งŽ์€ ๋ฐ์ดํ„ฐ ์ €์žฅ ๊ฐ€๋Šฅ

์„ธ์…˜ ๋‹จ์ :
โ€ข ์„œ๋ฒ„ ๋ฉ”๋ชจ๋ฆฌ/์ €์žฅ์†Œ ํ•„์š”
โ€ข ํ™•์žฅ์„ฑ ๊ณ ๋ ค ํ•„์š”

๐Ÿ”’ ์ถ”๊ฐ€ ๋ณด์•ˆ ๊ฐ•ํ™” ํŒ

๋งˆ์ง€๋ง‰์œผ๋กœ ๋ช‡ ๊ฐ€์ง€ ์ถ”๊ฐ€ ๋ณด์•ˆ ํŒ์„ ๊ณต์œ ํ• ๊ฒŒ. ์ด๊ฒƒ๋“ค๊นŒ์ง€ ์ ์šฉํ•˜๋ฉด ์ •๋ง ์•ˆ์ „ํ•œ ์‹œ์Šคํ…œ์„ ๋งŒ๋“ค ์ˆ˜ ์žˆ์–ด! ๐Ÿ›ก๏ธ

1. 2๋‹จ๊ณ„ ์ธ์ฆ (2FA)

๋น„๋ฐ€๋ฒˆํ˜ธ๋งŒ์œผ๋กœ๋Š” ๋ถ€์กฑํ•ด. ์ถ”๊ฐ€๋กœ SMS๋‚˜ ์ธ์ฆ ์•ฑ์„ ํ†ตํ•œ ์ฝ”๋“œ ์ž…๋ ฅ์„ ์š”๊ตฌํ•˜๋ฉด ๋ณด์•ˆ์ด ํ›จ์”ฌ ๊ฐ•ํ™”๋ผ.

<?php
// Google Authenticator ๊ฐ™์€ TOTP ๊ตฌํ˜„
class TwoFactorAuth {
    private $db;
    
    // 2FA ํ™œ์„ฑํ™”
    public function enable($userId) {
        // ๋น„๋ฐ€ ํ‚ค ์ƒ์„ฑ
        $secret = $this->generateSecret();
        
        // DB์— ์ €์žฅ
        $stmt = $this->db->prepare("
            UPDATE users 
            SET two_factor_secret = ?, two_factor_enabled = 1 
            WHERE id = ?
        ");
        $stmt->execute([$secret, $userId]);
        
        // QR ์ฝ”๋“œ URL ๋ฐ˜ํ™˜ (์‚ฌ์šฉ์ž๊ฐ€ ์Šค์บ”)
        return $this->getQRCodeUrl($userId, $secret);
    }
    
    // ์ฝ”๋“œ ๊ฒ€์ฆ
    public function verify($userId, $code) {
        $stmt = $this->db->prepare("
            SELECT two_factor_secret 
            FROM users 
            WHERE id = ? AND two_factor_enabled = 1
        ");
        $stmt->execute([$userId]);
        $user = $stmt->fetch();
        
        if(!$user) {
            return false;
        }
        
        // TOTP ์•Œ๊ณ ๋ฆฌ์ฆ˜์œผ๋กœ ์ฝ”๋“œ ๊ฒ€์ฆ
        return $this->verifyTOTP($user['two_factor_secret'], $code);
    }
    
    private function generateSecret($length = 16) {
        $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
        $secret = '';
        for($i = 0; $i < $length; $i++) {
            $secret .= $chars[random_int(0, strlen($chars) - 1)];
        }
        return $secret;
    }
    
    private function verifyTOTP($secret, $code) {
        // ์‹ค์ œ ๊ตฌํ˜„์€ TOTP ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์‚ฌ์šฉ ๊ถŒ์žฅ
        // ์˜ˆ: composer require spomky-labs/otphp
        $timeSlice = floor(time() / 30);
        
        // ์‹œ๊ฐ„ ์˜ค์ฐจ ๊ณ ๋ ค (ยฑ1 ํƒ€์ž„์Šฌ๋ผ์ด์Šค)
        for($i = -1; $i <= 1; $i++) {
            $calculatedCode = $this->calculateTOTP($secret, $timeSlice + $i);
            if($calculatedCode === $code) {
                return true;
            }
        }
        
        return false;
    }
}
?>

2. ๋กœ๊ทธ์ธ ์•Œ๋ฆผ

์ƒˆ๋กœ์šด ๊ธฐ๊ธฐ๋‚˜ ์œ„์น˜์—์„œ ๋กœ๊ทธ์ธํ•˜๋ฉด ์ด๋ฉ”์ผ์ด๋‚˜ SMS๋กœ ์•Œ๋ ค์ฃผ๋Š” ๊ฑฐ์•ผ. ์‚ฌ์šฉ์ž๊ฐ€ ์ž์‹ ์˜ ๊ณ„์ • ํ™œ๋™์„ ๋ชจ๋‹ˆํ„ฐ๋งํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ฃผ์ง€.

<?php
class LoginNotification {
    private $db;
    
    public function checkAndNotify($userId, $ipAddress, $userAgent) {
        // ์ตœ๊ทผ ๋กœ๊ทธ์ธ ๊ธฐ๋ก ํ™•์ธ
        $stmt = $this->db->prepare("
            SELECT ip_address, user_agent, created_at
            FROM login_history
            WHERE user_id = ?
            ORDER BY created_at DESC
            LIMIT 5
        ");
        $stmt->execute([$userId]);
        $recentLogins = $stmt->fetchAll();
        
        $isNewDevice = true;
        $isNewLocation = true;
        
        foreach($recentLogins as $login) {
            if($login['user_agent'] === $userAgent) {
                $isNewDevice = false;
            }
            if($this->isSameLocation($login['ip_address'], $ipAddress)) {
                $isNewLocation = false;
            }
        }
        
        // ์ƒˆ๋กœ์šด ๊ธฐ๊ธฐ ๋˜๋Š” ์œ„์น˜์—์„œ ๋กœ๊ทธ์ธ
        if($isNewDevice || $isNewLocation) {
            $this->sendNotification($userId, [
                'ip' => $ipAddress,
                'device' => $this->parseUserAgent($userAgent),
                'location' => $this->getLocation($ipAddress),
                'time' => date('Y-m-d H:i:s')
            ]);
        }
        
        // ๋กœ๊ทธ์ธ ๊ธฐ๋ก ์ €์žฅ
        $stmt = $this->db->prepare("
            INSERT INTO login_history (user_id, ip_address, user_agent, created_at)
            VALUES (?, ?, ?, NOW())
        ");
        $stmt->execute([$userId, $ipAddress, $userAgent]);
    }
    
    private function sendNotification($userId, $info) {
        // ์‚ฌ์šฉ์ž ์ด๋ฉ”์ผ ๊ฐ€์ ธ์˜ค๊ธฐ
        $stmt = $this->db->prepare("SELECT email, username FROM users WHERE id = ?");
        $stmt->execute([$userId]);
        $user = $stmt->fetch();
        
        $subject = "์ƒˆ๋กœ์šด ๋กœ๊ทธ์ธ ๊ฐ์ง€";
        $message = "
            ์•ˆ๋…•ํ•˜์„ธ์š”, {$user['username']}๋‹˜
            
            ์ƒˆ๋กœ์šด ๊ธฐ๊ธฐ์—์„œ ๋กœ๊ทธ์ธ์ด ๊ฐ์ง€๋˜์—ˆ์Šต๋‹ˆ๋‹ค:
            
            ์‹œ๊ฐ„: {$info['time']}
            ๊ธฐ๊ธฐ: {$info['device']}
            ์œ„์น˜: {$info['location']}
            IP: {$info['ip']}
            
            ๋ณธ์ธ์ด ์•„๋‹ˆ๋ผ๋ฉด ์ฆ‰์‹œ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ๋ณ€๊ฒฝํ•ด์ฃผ์„ธ์š”.
        ";
        
        // ์ด๋ฉ”์ผ ๋ฐœ์†ก
        mail($user['email'], $subject, $message);
    }
    
    private function isSameLocation($ip1, $ip2) {
        // IP ๊ธฐ๋ฐ˜ ์œ„์น˜ ๋น„๊ต (๊ฐ„๋‹จํ•œ ์˜ˆ์‹œ)
        // ์‹ค์ œ๋กœ๋Š” GeoIP ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์‚ฌ์šฉ
        $location1 = $this->getLocation($ip1);
        $location2 = $this->getLocation($ip2);
        
        return $location1 === $location2;
    }
    
    private function getLocation($ip) {
        // GeoIP ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์‚ฌ์šฉ ๊ถŒ์žฅ
        // ์˜ˆ: composer require geoip2/geoip2
        return "์„œ์šธ, ๋Œ€ํ•œ๋ฏผ๊ตญ"; // ์˜ˆ์‹œ
    }
    
    private function parseUserAgent($userAgent) {
        // User-Agent ํŒŒ์‹ฑ
        if(strpos($userAgent, 'Chrome') !== false) {
            return 'Chrome ๋ธŒ๋ผ์šฐ์ €';
        } elseif(strpos($userAgent, 'Firefox') !== false) {
            return 'Firefox ๋ธŒ๋ผ์šฐ์ €';
        } elseif(strpos($userAgent, 'Safari') !== false) {
            return 'Safari ๋ธŒ๋ผ์šฐ์ €';
        }
        return '์•Œ ์ˆ˜ ์—†๋Š” ๋ธŒ๋ผ์šฐ์ €';
    }
}
?>

3. Rate Limiting (์†๋„ ์ œํ•œ)

๋ฌด์ฐจ๋ณ„ ๋Œ€์ž… ๊ณต๊ฒฉ(Brute Force)์„ ๋ง‰๊ธฐ ์œ„ํ•ด ๋กœ๊ทธ์ธ ์‹œ๋„ ํšŸ์ˆ˜๋ฅผ ์ œํ•œํ•ด์•ผ ํ•ด. IP ์ฃผ์†Œ๋‹น ๋˜๋Š” ๊ณ„์ •๋‹น ์ œํ•œ์„ ๊ฑธ ์ˆ˜ ์žˆ์–ด.

<?php
class RateLimiter {
    private $redis;
    
    public function __construct($redis) {
        $this->redis = $redis;
    }
    
    // ์š”์ฒญ ํ—ˆ์šฉ ์—ฌ๋ถ€ ํ™•์ธ
    public function isAllowed($identifier, $maxAttempts = 5, $timeWindow = 300) {
        $key = "rate_limit:" . $identifier;
        $current = $this->redis->get($key);
        
        if($current === false) {
            // ์ฒซ ์‹œ๋„
            $this->redis->setex($key, $timeWindow, 1);
            return true;
        }
        
        if($current >= $maxAttempts) {
            // ์ œํ•œ ์ดˆ๊ณผ
            return false;
        }
        
        // ์นด์šดํŠธ ์ฆ๊ฐ€
        $this->redis->incr($key);
        return true;
    }
    
    // ๋‚จ์€ ์‹œ๋„ ํšŸ์ˆ˜
    public function getRemainingAttempts($identifier, $maxAttempts = 5) {
        $key = "rate_limit:" . $identifier;
        $current = $this->redis->get($key);
        
        if($current === false) {
            return $maxAttempts;
        }
        
        return max(0, $maxAttempts - $current);
    }
    
    // ์ œํ•œ ํ•ด์ œ๊นŒ์ง€ ๋‚จ์€ ์‹œ๊ฐ„
    public function getTimeUntilReset($identifier) {
        $key = "rate_limit:" . $identifier;
        return $this->redis->ttl($key);
    }
    
    // ์ œํ•œ ์ดˆ๊ธฐํ™”
    public function reset($identifier) {
        $key = "rate_limit:" . $identifier;
        $this->redis->del($key);
    }
}

// ์‚ฌ์šฉ ์˜ˆ์‹œ
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);

$rateLimiter = new RateLimiter($redis);

// ๋กœ๊ทธ์ธ ์‹œ๋„ ์ „ ํ™•์ธ
$ipAddress = $_SERVER['REMOTE_ADDR'];

if(!$rateLimiter->isAllowed($ipAddress, 5, 300)) {
    $timeLeft = $rateLimiter->getTimeUntilReset($ipAddress);
    die("๋„ˆ๋ฌด ๋งŽ์€ ๋กœ๊ทธ์ธ ์‹œ๋„๊ฐ€ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. {$timeLeft}์ดˆ ํ›„์— ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.");
}

// ๋กœ๊ทธ์ธ ์ฒ˜๋ฆฌ...
$remaining = $rateLimiter->getRemainingAttempts($ipAddress, 5);
echo "๋‚จ์€ ์‹œ๋„ ํšŸ์ˆ˜: {$remaining}";
?>

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

์™€! ์—ฌ๊ธฐ๊นŒ์ง€ ์ฝ์—ˆ๋‹ค๋ฉด ์ •๋ง ๋Œ€๋‹จํ•ด! ๐Ÿ‘ ์šฐ๋ฆฌ๊ฐ€ ๋‹ค๋ฃฌ ๋‚ด์šฉ์„ ์ •๋ฆฌํ•ด๋ณด์ž:

๐Ÿ“š ํ•ต์‹ฌ ์š”์•ฝ 1. ์ฟ ํ‚ค์™€ ์„ธ์…˜์˜ ์ฐจ์ด
โ€ข ์ฟ ํ‚ค: ํด๋ผ์ด์–ธํŠธ ์ €์žฅ, ์ž‘์€ ๋ฐ์ดํ„ฐ, ์‚ฌ์šฉ์ž๊ฐ€ ์ˆ˜์ • ๊ฐ€๋Šฅ
โ€ข ์„ธ์…˜: ์„œ๋ฒ„ ์ €์žฅ, ํฐ ๋ฐ์ดํ„ฐ, ์•ˆ์ „ํ•จ

2. ํ•„์ˆ˜ ๋ณด์•ˆ ์„ค์ •
โ€ข HTTPS ์‚ฌ์šฉ
โ€ข HttpOnly, Secure, SameSite ์ฟ ํ‚ค ํ”Œ๋ž˜๊ทธ
โ€ข ์„ธ์…˜ ID ์žฌ์ƒ์„ฑ
โ€ข CSRF ํ† ํฐ ๊ฒ€์ฆ

3. ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ
โ€ข Remember Me (Split Token Pattern)
โ€ข 2๋‹จ๊ณ„ ์ธ์ฆ
โ€ข Rate Limiting
โ€ข ๋กœ๊ทธ์ธ ์•Œ๋ฆผ

4. ํ™•์žฅ์„ฑ
โ€ข Redis/Memcached ์„ธ์…˜ ์ €์žฅ
โ€ข JWT ํ† ํฐ ๋ฐฉ์‹
โ€ข ๋‹ค์ค‘ ์„œ๋ฒ„ ํ™˜๊ฒฝ ๋Œ€์‘

PHP๋กœ ์›น ๊ฐœ๋ฐœ์„ ํ•˜๋ฉด์„œ ์„ธ์…˜๊ณผ ์ฟ ํ‚ค๋Š” ์ •๋ง ์ž์ฃผ ์‚ฌ์šฉํ•˜๊ฒŒ ๋ผ. ํŠนํžˆ ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ ์‚ฌ์šฉ์ž ์ธ์ฆ์€ ํ•ต์‹ฌ ๊ธฐ๋Šฅ์ด์ž–์•„? ์ด ๊ธ€์—์„œ ๋ฐฐ์šด ๋‚ด์šฉ๋“ค์„ ์‹ค์ œ ํ”„๋กœ์ ํŠธ์— ์ ์šฉํ•˜๋ฉด, ์•ˆ์ „ํ•˜๊ณ  ํ™•์žฅ ๊ฐ€๋Šฅํ•œ ์‹œ์Šคํ…œ์„ ๋งŒ๋“ค ์ˆ˜ ์žˆ์„ ๊ฑฐ์•ผ. ๐Ÿ’ช

๋ณด์•ˆ์€ ํ•œ ๋ฒˆ ์„ค์ •ํ•˜๊ณ  ๋๋‚˜๋Š” ๊ฒŒ ์•„๋‹ˆ์•ผ. ๊ณ„์†ํ•ด์„œ ์ƒˆ๋กœ์šด ์ทจ์•ฝ์ ์ด ๋ฐœ๊ฒฌ๋˜๊ณ , ๊ณต๊ฒฉ ๊ธฐ๋ฒ•๋„ ์ง„ํ™”ํ•˜๊ฑฐ๋“ . ๊ทธ๋ž˜์„œ ํ•ญ์ƒ ์ตœ์‹  ๋ณด์•ˆ ๋™ํ–ฅ์„ ์ฒดํฌํ•˜๊ณ , ์ฝ”๋“œ๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๋Š” ๊ฒŒ ์ค‘์š”ํ•ด. PHP ๊ณต์‹ ๋ฌธ์„œ๋‚˜ OWASP ๊ฐ™์€ ๋ณด์•ˆ ์ปค๋ฎค๋‹ˆํ‹ฐ๋ฅผ ์ž์ฃผ ํ™•์ธํ•˜๋Š” ์Šต๊ด€์„ ๋“ค์ด๋ฉด ์ข‹์•„!

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

๊ถ๊ธˆํ•œ ์ ์ด ์žˆ๊ฑฐ๋‚˜ ๋” ๊นŠ์ด ์žˆ๋Š” ๋‚ด์šฉ์„ ์•Œ๊ณ  ์‹ถ๋‹ค๋ฉด, ์žฌ๋Šฅ๋„ท์˜ ์ง€์‹์ธ์˜ ์ˆฒ์—์„œ ๋‹ค๋ฅธ ๊ฐœ๋ฐœ์ž๋“ค๊ณผ ์†Œํ†ตํ•ด๋ณด๋Š” ๊ฒƒ๋„ ์ข‹์€ ๋ฐฉ๋ฒ•์ด์•ผ. ํ•จ๊ป˜ ๋ฐฐ์šฐ๊ณ  ์„ฑ์žฅํ•˜๋Š” ๊ฒŒ ๊ฐœ๋ฐœ์ž ์ปค๋ฎค๋‹ˆํ‹ฐ์˜ ๋งค๋ ฅ์ด๋‹ˆ๊นŒ! ๐Ÿ˜Š

๊ทธ๋Ÿผ ์•ˆ์ „ํ•˜๊ณ  ๋ฉ‹์ง„ ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๊ฐœ๋ฐœํ•˜๊ธธ ๋ฐ”๋ž„๊ฒŒ! ํ™”์ดํŒ…! ๐Ÿš€

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

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

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

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