Files
financial/app/lib/auth.php
T
2026-06-07 00:33:58 +09:00

285 lines
7.0 KiB
PHP

<?php
require_once __DIR__ . '/db.php';
if (session_status() === PHP_SESSION_NONE) {
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'secure' => !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off',
'httponly' => true,
'samesite' => 'Lax',
]);
session_start();
}
function send_private_no_store_headers(): void
{
if (headers_sent()) {
return;
}
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
header('Pragma: no-cache');
header('Expires: 0');
header('X-Content-Type-Options: nosniff');
header('Referrer-Policy: same-origin');
}
function csrf_token(): string
{
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrf_token'];
}
function csrf_field(): string
{
return '<input type="hidden" name="csrf_token" value="' .
htmlspecialchars(csrf_token(), ENT_QUOTES, 'UTF-8') .
'">';
}
function verify_csrf_token(?string $token): bool
{
return is_string($token)
&& isset($_SESSION['csrf_token'])
&& hash_equals($_SESSION['csrf_token'], $token);
}
function require_valid_csrf_for_post(): void
{
if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') {
return;
}
if (verify_csrf_token($_POST['csrf_token'] ?? null)) {
return;
}
http_response_code(419);
exit('Invalid CSRF token.');
}
function enable_csrf_form_injection(): void
{
if (PHP_SAPI === 'cli' || defined('FINANCIAL_CSRF_INJECTION_ENABLED')) {
return;
}
define('FINANCIAL_CSRF_INJECTION_ENABLED', true);
ob_start(static function (string $buffer): string {
if (
stripos($buffer, '<form') === false ||
!preg_match('/method\s*=\s*["\']?post/i', $buffer)
) {
return $buffer;
}
$field = csrf_field();
return preg_replace_callback(
'/<form\b([^>]*)>/i',
static function (array $matches) use ($field): string {
$tag = $matches[0];
if (
stripos($tag, 'method="post"') === false &&
stripos($tag, "method='post'") === false &&
!preg_match('/method\s*=\s*post/i', $tag)
) {
return $tag;
}
if (stripos($tag, 'csrf_token') !== false) {
return $tag;
}
return $tag . $field;
},
$buffer
) ?? $buffer;
});
}
function throttle_login_attempts(string $username): void
{
$key = 'login_attempts_' . hash('sha256', strtolower($username) . '|' . ($_SERVER['REMOTE_ADDR'] ?? ''));
$now = time();
$attempt = $_SESSION[$key] ?? ['count' => 0, 'first_at' => $now];
if (($now - (int)$attempt['first_at']) > 900) {
$attempt = ['count' => 0, 'first_at' => $now];
}
if ((int)$attempt['count'] >= 8) {
throw new RuntimeException('로그인 시도가 많습니다. 잠시 후 다시 시도하세요.');
}
$attempt['count']++;
$_SESSION[$key] = $attempt;
}
function clear_login_attempts(string $username): void
{
$key = 'login_attempts_' . hash('sha256', strtolower($username) . '|' . ($_SERVER['REMOTE_ADDR'] ?? ''));
unset($_SESSION[$key]);
}
function login_user(array $user, bool $remember = false): void
{
session_regenerate_id(true);
$_SESSION['user_id'] = (int)$user['id'];
$_SESSION['username'] = $user['username'];
if ($remember) {
$token = bin2hex(random_bytes(32));
$tokenHash = hash('sha256', $token);
$expiresAt = date('Y-m-d H:i:s', strtotime('+30 days'));
$pdo = db();
$stmt = $pdo->prepare("
UPDATE users
SET remember_token = ?, remember_expires_at = ?
WHERE id = ?
");
$stmt->execute([$tokenHash, $expiresAt, $user['id']]);
setcookie(
'remember_token',
$token,
[
'expires' => strtotime('+30 days'),
'path' => '/',
'secure' => true,
'httponly' => true,
'samesite' => 'Lax',
]
);
}
}
function logout_user(): void
{
if (!empty($_SESSION['user_id'])) {
$pdo = db();
$stmt = $pdo->prepare("
UPDATE users
SET remember_token = NULL, remember_expires_at = NULL
WHERE id = ?
");
$stmt->execute([$_SESSION['user_id']]);
}
setcookie(
'remember_token',
'',
[
'expires' => time() - 3600,
'path' => '/',
'secure' => true,
'httponly' => true,
'samesite' => 'Lax',
]
);
$_SESSION = [];
if (ini_get('session.use_cookies')) {
$params = session_get_cookie_params();
setcookie(session_name(), '', time() - 42000, $params['path'], $params['domain'] ?? '', $params['secure'] ?? false, $params['httponly'] ?? false);
}
session_destroy();
}
function try_auto_login(): void
{
if (!empty($_SESSION['user_id'])) {
return;
}
if (empty($_COOKIE['remember_token'])) {
return;
}
$token = $_COOKIE['remember_token'];
$tokenHash = hash('sha256', $token);
$pdo = db();
$stmt = $pdo->prepare("
SELECT *
FROM users
WHERE remember_token = ?
AND remember_expires_at IS NOT NULL
AND remember_expires_at > NOW()
LIMIT 1
");
$stmt->execute([$tokenHash]);
$user = $stmt->fetch();
if (!$user) {
setcookie(
'remember_token',
'',
[
'expires' => time() - 3600,
'path' => '/',
'secure' => true,
'httponly' => true,
'samesite' => 'Lax',
]
);
return;
}
$_SESSION['user_id'] = (int)$user['id'];
$_SESSION['username'] = $user['username'];
$newToken = bin2hex(random_bytes(32));
$newHash = hash('sha256', $newToken);
$expiresAt = date('Y-m-d H:i:s', strtotime('+30 days'));
$stmt = $pdo->prepare("
UPDATE users
SET remember_token = ?, remember_expires_at = ?
WHERE id = ?
");
$stmt->execute([$newHash, $expiresAt, $user['id']]);
setcookie(
'remember_token',
$newToken,
[
'expires' => strtotime('+30 days'),
'path' => '/',
'secure' => true,
'httponly' => true,
'samesite' => 'Lax',
]
);
}
function check_auth(): void
{
try_auto_login();
if (empty($_SESSION['user_id'])) {
header('Location: /login.php');
exit;
}
send_private_no_store_headers();
}
function user_id(): int
{
return (int)($_SESSION['user_id'] ?? 0);
}
send_private_no_store_headers();
require_valid_csrf_for_post();
enable_csrf_form_injection();