0, 'path' => '/', 'domain' => '', 'secure' => $secure, 'httponly' => true, 'samesite' => 'Lax', ]); session_start(); } } /** 获取(或创建)当前会话绑定的 CSRF Token */ function csrfToken() { startSessionSecure(); if (empty($_SESSION['csrf_token']) || strlen($_SESSION['csrf_token']) !== 64) { $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); } return $_SESSION['csrf_token']; } /** * 校验写接口的 CSRF Token(要求 X-CSRF-Token 请求头与会话一致) * 配合前端 common.js httpPost 统一附带;防跨站请求伪造(REV-2/REV-SEC-2) */ function checkCsrf() { startSessionSecure(); $token = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? ''; if ($token === '' || empty($_SESSION['csrf_token']) || !hash_equals($_SESSION['csrf_token'], $token)) { Response::error('安全校验失败(CSRF),请刷新页面后重试', 403); } } /** * 登录限流:同一 IP+账号 15 分钟内失败 ≥5 次则锁定(429) * fail-open:DB 异常时跳过限流,保证登录可用(REV-8) */ function checkLoginThrottle($username, $ip) { try { $pdo = DB::getInstance()->getPdo(); $pdo->prepare("DELETE FROM system_login_attempts WHERE attempt_time < NOW() - INTERVAL 30 MINUTE")->execute(); $stmt = $pdo->prepare( "SELECT COUNT(*) FROM system_login_attempts WHERE ip = ? AND username = ? AND success = 0 AND attempt_time > NOW() - INTERVAL 15 MINUTE" ); $stmt->execute([$ip, $username]); if ((int)$stmt->fetchColumn() >= 5) { Response::error('尝试次数过多,请15分钟后再试', 429); } } catch (Exception $e) { logError('登录限流检查失败(已跳过限流)', ['msg' => $e->getMessage()]); } } /** 记录一次登录尝试结果(供限流统计) */ function recordLoginAttempt($username, $ip, $success) { try { $pdo = DB::getInstance()->getPdo(); $pdo->prepare( "INSERT INTO system_login_attempts (username, ip, success, attempt_time) VALUES (?, ?, ?, NOW())" )->execute([$username, $ip, $success ? 1 : 0]); } catch (Exception $e) { logError('登录尝试记录失败', ['msg' => $e->getMessage()]); } }