Files
superlink/api/common/security.php
T

85 lines
2.9 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* 安全中间件(批次1):Session 加固 + CSRF Token + 登录限流
* 依赖:db.php / response.php / logger.php(均无回环依赖)
*/
require_once __DIR__ . '/db.php';
require_once __DIR__ . '/response.php';
require_once __DIR__ . '/logger.php';
/** 安全启动 Session:httponly + samesite=Lax + HTTPS 下 secure */
function startSessionSecure()
{
if (session_status() === PHP_SESSION_NONE) {
$secure = !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off';
session_set_cookie_params([
'lifetime' => 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()]);
}
}