Ver3.0 ๐ PHP ์ธ์ ๊ด๋ฆฌ์ ์ฟ ํค ๋ณด์์ผ๋ก ์ฌ์ฉ์ ์ํ๋ฅผ ์๋ฒฝํ๊ฒ ์งํค๋ ๋ฐฉ๋ฒ

๐ PHP ์ธ์ ๊ด๋ฆฌ์ ์ฟ ํค ๋ณด์์ผ๋ก ์ฌ์ฉ์ ์ํ๋ฅผ ์๋ฒฝํ๊ฒ ์งํค๋ ๋ฐฉ๋ฒ
์น ๊ฐ๋ฐ์๋ผ๋ฉด ๋ฐ๋์ ์์์ผ ํ ์ฌ์ฉ์ ์ธ์ฆ๊ณผ ๋ณด์ ํต์ฌ ๊ฐ์ด๋
์๋ ! ๐ ์ค๋์ PHP๋ก ์น ๊ฐ๋ฐํ ๋ ์ ๋ง ์ค์ํ ์ฃผ์ ๋ฅผ ๋ค๋ค๋ณผ ๊ฑฐ์ผ. ๋ฐ๋ก ์ธ์ ๊ด๋ฆฌ์ ์ฟ ํค ๋ณด์์ด์ง. ์ฌ์ค ์ด๊ฑฐ, ์ฒ์ ๋ฐฐ์ธ ๋ "๊ทธ๋ฅ ๋ก๊ทธ์ธ ๊ธฐ๋ฅ ๋ง๋๋ ๊ฑฐ ์๋์ผ?"๋ผ๊ณ ์๊ฐํ๊ธฐ ์ฌ์ด๋ฐ, ๋ง์ ์ ๋๋ก ํ๊ณ ๋ค๋ฉด ๋ณด์ ์ทจ์ฝ์ ์ด ํ๋ ๊ฐ๊ฐ ์๋๋๋ผ๊ณ . ๐
์น์ฌ์ดํธ์์ ๋ก๊ทธ์ธํ๋ฉด ํ์ด์ง๋ฅผ ์ด๋ํด๋ ๋ก๊ทธ์ธ ์ํ๊ฐ ์ ์ง๋์์? ์ด๊ฒ ๋ฐ๋ก ์ธ์ ๊ณผ ์ฟ ํค ๋๋ถ์ด์ผ. ๊ทผ๋ฐ ์ด๊ฑธ ์๋ชป ๊ตฌํํ๋ฉด ํด์ปค๋ค์ด ์ฌ์ฉ์ ๊ณ์ ์ ํ์ทจํ๊ฑฐ๋, ๊ฐ์ธ์ ๋ณด๋ฅผ ๋นผ๊ฐ๋ ์ผ์ด ์๊ธธ ์ ์์ด. ์ค์ ๋ก ๋ง์ ์น์ฌ์ดํธ๋ค์ด ์ด๋ฐ ๋ณด์ ๋ฌธ์ ๋ก ๊ณค๋์ ๊ฒช๊ณ ์์ง.
๐ช ์ฟ ํค(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();
?>
session_start()๋ ํ์ด์ง ์ต์๋จ์์ ํธ์ถํด์ผ ํด. HTML ํ๊ทธ๋ ๊ณต๋ฐฑ, ์ฌ์ง์ด BOM(Byte Order Mark)๋ ์์ผ๋ฉด ์ ๋ผ. "Headers already sent" ์๋ฌ๊ฐ ๋๋ฉด ๋๋ถ๋ถ ์ด๊ฒ ์์ธ์ด์ผ!
์ธ์ ์ ์ฅ ์์น
PHP๋ ๊ธฐ๋ณธ์ ์ผ๋ก ์ธ์
๋ฐ์ดํฐ๋ฅผ ์๋ฒ์ ์์ ๋๋ ํ ๋ฆฌ์ ํ์ผ๋ก ์ ์ฅํด. ๋ฆฌ๋
์ค๋ผ๋ฉด ๋ณดํต /tmp ๋๋ ํ ๋ฆฌ์ sess_์ธ์
ID ํํ๋ก ์ ์ฅ๋์ง. ํ์ง๋ง ํธ๋ํฝ์ด ๋ง์ ์ฌ์ดํธ๋ผ๋ฉด ์ด ๋ฐฉ์์ ๋นํจ์จ์ ์ด์ผ.
| ์ ์ฅ ๋ฐฉ์ | ์ฅ์ | ๋จ์ | ์ถ์ฒ ์ํฉ |
|---|---|---|---|
| ํ์ผ | ์ค์ ๊ฐ๋จ, ๊ธฐ๋ณธ๊ฐ | ์๋ ๋๋ฆผ, ํ์ฅ์ฑ ๋ฎ์ | ์๊ท๋ชจ ์ฌ์ดํธ |
| ๋ฐ์ดํฐ๋ฒ ์ด์ค | ์๊ตฌ ๋ณด๊ด, ๊ด๋ฆฌ ์ฉ์ด | DB ๋ถํ ์ฆ๊ฐ | ์ค๊ท๋ชจ ์ฌ์ดํธ |
| Redis/Memcached | ๋งค์ฐ ๋น ๋ฆ, ํ์ฅ์ฑ ์ฐ์ | ๋ณ๋ ์๋ฒ ํ์ | ๋๊ท๋ชจ ์ฌ์ดํธ |
์ฌ๋ฅ๋ท ๊ฐ์ ํ๋ซํผ์์๋ ์ฌ์ฉ์๊ฐ ๋ง์์ง๋ฉด ์ธ์ ๊ด๋ฆฌ๊ฐ ์ ๋ง ์ค์ํด์ ธ. ์์ฒ ๋ช ์ด ๋์์ ์ ์ํ๋ ์ํฉ์์ ํ์ผ ๊ธฐ๋ฐ ์ธ์ ์ ์ฐ๋ฉด ์๋ฒ๊ฐ ๋ฒํฐ๊ธฐ ํ๋ค๊ฑฐ๋ . ๊ทธ๋์ Redis ๊ฐ์ ์ธ๋ฉ๋ชจ๋ฆฌ ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ์ฌ์ฉํ๋ ๊ฒ ์ผ๋ฐ์ ์ด์ผ. โก
๐ก๏ธ ์ธ์ ๋ณด์: ์ธ์ ํ์ด์ฌํน ๋ฐฉ์ดํ๊ธฐ
์, ์ด์ ์ง์ง ์ค์ํ ๋ณด์ ์๊ธฐ๋ฅผ ํด๋ณด์. ์ธ์ ํ์ด์ฌํน(Session Hijacking)์ด๋ผ๊ณ ๋ค์ด๋ดค์ด? ํด์ปค๊ฐ ๋ค๋ฅธ ์ฌ๋์ ์ธ์ ID๋ฅผ ํ์ณ์ ๊ทธ ์ฌ๋์ธ ์ฒ ํ๋ ๊ณต๊ฒฉ์ด์ผ. ๐ฑ
์์ํด๋ด. ๋ค๊ฐ ์นดํ์์ ๊ณต์ฉ ์์ดํ์ด๋ก ๋ก๊ทธ์ธํ๋๋ฐ, ์์ ์๋ ํด์ปค๊ฐ ๋คํธ์ํฌ ํจํท์ ๊ฐ๋ก์ฑ์ ์ธ์ ID๋ฅผ ํ์ณค์ด. ๊ทธ๋ผ ๊ทธ ํด์ปค๋ ๋ค ๊ณ์ ์ผ๋ก ๋ญ๋ ์ง ํ ์ ์๊ฒ ๋๋ ๊ฑฐ์ง. ๋ฌด์ญ์ง?
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) ๊ณต๊ฒฉ์ ๋ง์์ค. ์ด ์์ฑ์ ์ธ ๊ฐ์ง ๊ฐ์ ๊ฐ์ง ์ ์์ด:
โข 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);
?>
โฐ ์ธ์ ํ์์์ ๊ด๋ฆฌ
์ฌ์ฉ์๊ฐ ๋ก๊ทธ์ธํ ์ฑ๋ก ์๋ฆฌ๋ฅผ ๋น์ ๋๋ฐ, ๋ค๋ฅธ ์ฌ๋์ด ๊ทธ ์ปดํจํฐ๋ฅผ ์ฌ์ฉํ๋ฉด ์ด๋ป๊ฒ ๋ ๊น? ๐ฑ ์ด๋ฐ ์ํฉ์ ๋ง๊ธฐ ์ํด ์ธ์ ํ์์์์ ์ค์ ํด์ผ ํด. ์ผ์ ์๊ฐ ๋์ ํ๋์ด ์์ผ๋ฉด ์๋์ผ๋ก ๋ก๊ทธ์์์ํค๋ ๊ฑฐ์ง.
<?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)
);
*/
?>
๐ CSRF ๊ณต๊ฒฉ๊ณผ ๋ฐฉ์ด
CSRF(Cross-Site Request Forgery)๋ ์ฌ์ฉ์๊ฐ ์๋ํ์ง ์์ ์์ฒญ์ ๋ณด๋ด๋๋ก ๋ง๋๋ ๊ณต๊ฒฉ์ด์ผ. ์๋ฅผ ๋ค์ด, ์ ์ฑ ์ฌ์ดํธ์ ์๋ ์ด๋ฏธ์ง ํ๊ทธ๊ฐ ์ค์ ๋ก๋ ์ํ ์ก๊ธ ์์ฒญ์ ๋ณด๋ด๋ ์์ด์ง. ๐
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 ๋จ์ :
โข ํ ํฐ ํฌ๊ธฐ๊ฐ ํผ (๋งค ์์ฒญ๋ง๋ค ์ ์ก)
โข ํ ํฐ ๋ฌดํจํ๊ฐ ์ด๋ ค์
โข ๋ฏผ๊ฐํ ์ ๋ณด ์ ์ฅ ๋ถ๊ฐ
์ธ์ ์ฅ์ :
โข ์๋ฒ์์ ์์ ํ ์ ์ด ๊ฐ๋ฅ
โข ์ฆ์ ๋ฌดํจํ ๊ฐ๋ฅ
โข ๋ ๋ง์ ๋ฐ์ดํฐ ์ ์ฅ ๊ฐ๋ฅ
์ธ์ ๋จ์ :
โข ์๋ฒ ๋ฉ๋ชจ๋ฆฌ/์ ์ฅ์ ํ์
โข ํ์ฅ์ฑ ๊ณ ๋ ค ํ์
๐ ์ถ๊ฐ ๋ณด์ ๊ฐํ ํ
๋ง์ง๋ง์ผ๋ก ๋ช ๊ฐ์ง ์ถ๊ฐ ๋ณด์ ํ์ ๊ณต์ ํ ๊ฒ. ์ด๊ฒ๋ค๊น์ง ์ ์ฉํ๋ฉด ์ ๋ง ์์ ํ ์์คํ ์ ๋ง๋ค ์ ์์ด! ๐ก๏ธ
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}";
?>
๐ ๋ง๋ฌด๋ฆฌํ๋ฉฐ
์! ์ฌ๊ธฐ๊น์ง ์ฝ์๋ค๋ฉด ์ ๋ง ๋๋จํด! ๐ ์ฐ๋ฆฌ๊ฐ ๋ค๋ฃฌ ๋ด์ฉ์ ์ ๋ฆฌํด๋ณด์:
โข ์ฟ ํค: ํด๋ผ์ด์ธํธ ์ ์ฅ, ์์ ๋ฐ์ดํฐ, ์ฌ์ฉ์๊ฐ ์์ ๊ฐ๋ฅ
โข ์ธ์ : ์๋ฒ ์ ์ฅ, ํฐ ๋ฐ์ดํฐ, ์์ ํจ
2. ํ์ ๋ณด์ ์ค์
โข HTTPS ์ฌ์ฉ
โข HttpOnly, Secure, SameSite ์ฟ ํค ํ๋๊ทธ
โข ์ธ์ ID ์ฌ์์ฑ
โข CSRF ํ ํฐ ๊ฒ์ฆ
3. ๊ณ ๊ธ ๊ธฐ๋ฅ
โข Remember Me (Split Token Pattern)
โข 2๋จ๊ณ ์ธ์ฆ
โข Rate Limiting
โข ๋ก๊ทธ์ธ ์๋ฆผ
4. ํ์ฅ์ฑ
โข Redis/Memcached ์ธ์ ์ ์ฅ
โข JWT ํ ํฐ ๋ฐฉ์
โข ๋ค์ค ์๋ฒ ํ๊ฒฝ ๋์
PHP๋ก ์น ๊ฐ๋ฐ์ ํ๋ฉด์ ์ธ์ ๊ณผ ์ฟ ํค๋ ์ ๋ง ์์ฃผ ์ฌ์ฉํ๊ฒ ๋ผ. ํนํ ์ฌ๋ฅ๋ท ๊ฐ์ ํ๋ซํผ์์ ์ฌ์ฉ์ ์ธ์ฆ์ ํต์ฌ ๊ธฐ๋ฅ์ด์์? ์ด ๊ธ์์ ๋ฐฐ์ด ๋ด์ฉ๋ค์ ์ค์ ํ๋ก์ ํธ์ ์ ์ฉํ๋ฉด, ์์ ํ๊ณ ํ์ฅ ๊ฐ๋ฅํ ์์คํ ์ ๋ง๋ค ์ ์์ ๊ฑฐ์ผ. ๐ช
๋ณด์์ ํ ๋ฒ ์ค์ ํ๊ณ ๋๋๋ ๊ฒ ์๋์ผ. ๊ณ์ํด์ ์๋ก์ด ์ทจ์ฝ์ ์ด ๋ฐ๊ฒฌ๋๊ณ , ๊ณต๊ฒฉ ๊ธฐ๋ฒ๋ ์งํํ๊ฑฐ๋ . ๊ทธ๋์ ํญ์ ์ต์ ๋ณด์ ๋ํฅ์ ์ฒดํฌํ๊ณ , ์ฝ๋๋ฅผ ์ ๋ฐ์ดํธํ๋ ๊ฒ ์ค์ํด. PHP ๊ณต์ ๋ฌธ์๋ OWASP ๊ฐ์ ๋ณด์ ์ปค๋ฎค๋ํฐ๋ฅผ ์์ฃผ ํ์ธํ๋ ์ต๊ด์ ๋ค์ด๋ฉด ์ข์!
๋ง์ง๋ง์ผ๋ก, ์๋ฒฝํ ๋ณด์์ ์๋ค๋ ๊ฑธ ๊ธฐ์ตํด. ํ์ง๋ง ์ด ๊ธ์์ ๋ค๋ฃฌ ๊ธฐ๋ฒ๋ค์ ์ ์ ์ฉํ๋ฉด, ๋๋ถ๋ถ์ ์ผ๋ฐ์ ์ธ ๊ณต๊ฒฉ์ ๋ง์ ์ ์์ด. ์ฌ์ฉ์๋ค์ ์์คํ ๋ฐ์ดํฐ๋ฅผ ์งํค๋ ๊ฑด ๊ฐ๋ฐ์์ ์ฑ ์์ด๋๊น, ํญ์ ๋ณด์์ ์ต์ฐ์ ์ผ๋ก ์๊ฐํ์! ๐
๊ถ๊ธํ ์ ์ด ์๊ฑฐ๋ ๋ ๊น์ด ์๋ ๋ด์ฉ์ ์๊ณ ์ถ๋ค๋ฉด, ์ฌ๋ฅ๋ท์ ์ง์์ธ์ ์ฒ์์ ๋ค๋ฅธ ๊ฐ๋ฐ์๋ค๊ณผ ์ํตํด๋ณด๋ ๊ฒ๋ ์ข์ ๋ฐฉ๋ฒ์ด์ผ. ํจ๊ป ๋ฐฐ์ฐ๊ณ ์ฑ์ฅํ๋ ๊ฒ ๊ฐ๋ฐ์ ์ปค๋ฎค๋ํฐ์ ๋งค๋ ฅ์ด๋๊น! ๐
๊ทธ๋ผ ์์ ํ๊ณ ๋ฉ์ง ์น ์ ํ๋ฆฌ์ผ์ด์ ๊ฐ๋ฐํ๊ธธ ๋ฐ๋๊ฒ! ํ์ดํ ! ๐
๊ด๋ จ ํค์๋
๋๊ธ ์์ฑ
์ด ๊ธ์ ๋ํ ์ฌ๋ฌ๋ถ์ ์๊ฐ์ ๋ค๋ ค์ฃผ์ธ์
๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค
๋๊ธ์ ์์ฑํ๋ ค๋ฉด ๋จผ์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.
๋๊ธ 0
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.
์์ง ๋๊ธ์ด ์์ต๋๋ค.