上次错误
This commit is contained in:
+21
-123
@@ -561,15 +561,9 @@ if (!function_exists('site_url')) {
|
||||
{
|
||||
if (headers_sent()) return;
|
||||
$nonce = csp_nonce();
|
||||
// 通用安全响应头从 PHP 兜底补齐:即便 Nginx 层未下发也不会缺失(防配置漂移)。
|
||||
// 与审计整改要求一致:补充 X-Content-Type-Options / Referrer-Policy / Permissions-Policy,
|
||||
// 并将 HSTS 升级为含 includeSubDomains + preload。若 Nginx 也下发 HSTS,重复为无害,
|
||||
// 浏览器取更严格项(max-age 取最大值并合并指令)。
|
||||
header("X-Content-Type-Options: nosniff");
|
||||
header("Referrer-Policy: strict-origin-when-cross-origin");
|
||||
header("Permissions-Policy: geolocation=(), camera=(), microphone=(), payment=()");
|
||||
header("Strict-Transport-Security: max-age=63072000; includeSubDomains; preload");
|
||||
// 严格 CSP(nonce 每次请求不同,必须走 PHP)
|
||||
// 通用安全响应头(HSTS / X-Frame-Options / X-Content-Type-Options 等)已统一在
|
||||
// Nginx 服务器层下发(含静态资源),无需在此重复。
|
||||
// 此处仅补充依赖动态随机数的「严格 CSP」——nonce 每次请求不同,必须走 PHP。
|
||||
header("Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-{$nonce}'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; frame-ancestors 'none'; base-uri 'self'; form-action 'self'");
|
||||
}
|
||||
|
||||
@@ -698,89 +692,35 @@ if (!function_exists('site_url')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/* ---------- IP 级登录限速(fail2ban 式,文件缓存,越会话更抗爆破) ----------
|
||||
* 双窗口独立限速(按需求定制):
|
||||
* · 失败登录:任意 10 分钟内最多 5 次;超出即封锁 30 分钟
|
||||
* · 成功登录:任意 30 分钟内最多 5 次;超出即限制(封锁至最早成功滑出 30 分钟窗口)
|
||||
* 数据文件:storage/login_ip.json —— 每个 IP 记失败/成功时间戳列表 + 封锁截止时间
|
||||
* --------------------------------------------------------------------- */
|
||||
defined('LOGIN_FAIL_WINDOW') or define('LOGIN_FAIL_WINDOW', 600); // 失败计数窗口:10 分钟
|
||||
defined('LOGIN_FAIL_LIMIT') or define('LOGIN_FAIL_LIMIT', 5); // 失败次数上限
|
||||
defined('LOGIN_OK_WINDOW') or define('LOGIN_OK_WINDOW', 1800); // 成功计数窗口:30 分钟
|
||||
defined('LOGIN_OK_LIMIT') or define('LOGIN_OK_LIMIT', 5); // 成功次数上限
|
||||
defined('LOGIN_BLOCK_SECS') or define('LOGIN_BLOCK_SECS', 1800); // 超限后封锁时长:30 分钟
|
||||
|
||||
function _ip_login_load(): array
|
||||
/* ---------- IP 级失败限速(fail2ban 式,文件缓存,越会话更抗爆破) ---------- */
|
||||
function ip_login_blocked(string $ip): bool
|
||||
{
|
||||
$file = BASE_PATH . '/storage/login_ip.json';
|
||||
return is_file($file) ? (json_decode(@file_get_contents($file), true) ?: []) : [];
|
||||
if (!is_file($file)) return false;
|
||||
$data = json_decode(@file_get_contents($file), true) ?: [];
|
||||
$now = time();
|
||||
if (!isset($data[$ip])) return false;
|
||||
return $data[$ip]['count'] >= 8;
|
||||
}
|
||||
function _ip_login_save(array $data): void
|
||||
function ip_login_register(string $ip): void
|
||||
{
|
||||
$file = BASE_PATH . '/storage/login_ip.json';
|
||||
if (!is_dir(dirname($file))) @mkdir(dirname($file), 0755, true);
|
||||
@file_put_contents($file, json_encode($data));
|
||||
}
|
||||
/** 裁剪过期时间戳并按规则重算封锁截止时间(就地修改 $st) */
|
||||
function _ip_login_prune(array &$st, int $now): void
|
||||
{
|
||||
$st['fail'] = array_values(array_filter((array)($st['fail'] ?? []), fn($t) => ($now - (int)$t) < LOGIN_FAIL_WINDOW));
|
||||
$st['ok'] = array_values(array_filter((array)($st['ok'] ?? []), fn($t) => ($now - (int)$t) < LOGIN_OK_WINDOW));
|
||||
if (!isset($st['block_until']) || !is_numeric($st['block_until'])) $st['block_until'] = 0;
|
||||
if ($st['block_until'] <= $now) {
|
||||
if (count($st['fail']) >= LOGIN_FAIL_LIMIT) {
|
||||
// 失败 5 次 / 10 分钟 → 锁 30 分钟
|
||||
$st['block_until'] = $now + LOGIN_BLOCK_SECS;
|
||||
} elseif (count($st['ok']) >= LOGIN_OK_LIMIT) {
|
||||
// 成功 5 次 / 30 分钟 → 锁到最早一次成功滑出窗口
|
||||
$oldest = min($st['ok']);
|
||||
$st['block_until'] = max($now + 60, $oldest + LOGIN_OK_WINDOW);
|
||||
}
|
||||
$data = is_file($file) ? (json_decode(@file_get_contents($file), true) ?: []) : [];
|
||||
$now = time();
|
||||
if (!isset($data[$ip]) || ($data[$ip]['time'] + 900) < $now) {
|
||||
$data[$ip] = ['count' => 0, 'time' => $now];
|
||||
}
|
||||
}
|
||||
function ip_login_blocked(string $ip): bool
|
||||
{
|
||||
$now = time();
|
||||
$st = _ip_login_load()[$ip] ?? ['fail' => [], 'ok' => [], 'block_until' => 0];
|
||||
_ip_login_prune($st, $now);
|
||||
return ($st['block_until'] ?? 0) > $now;
|
||||
}
|
||||
/** 返回剩余封锁秒数(已解封为 0),供 Retry-After 使用 */
|
||||
function ip_login_remaining(string $ip): int
|
||||
{
|
||||
$now = time();
|
||||
$st = _ip_login_load()[$ip] ?? ['fail' => [], 'ok' => [], 'block_until' => 0];
|
||||
_ip_login_prune($st, $now);
|
||||
return max(0, (int)($st['block_until'] ?? 0) - $now);
|
||||
}
|
||||
function ip_login_register_fail(string $ip): void
|
||||
{
|
||||
$now = time();
|
||||
$data = _ip_login_load();
|
||||
$st = $data[$ip] ?? ['fail' => [], 'ok' => [], 'block_until' => 0];
|
||||
_ip_login_prune($st, $now);
|
||||
$st['fail'][] = $now;
|
||||
_ip_login_prune($st, $now); // 追加后重新评估是否触发封锁
|
||||
$data[$ip] = $st;
|
||||
_ip_login_save($data);
|
||||
}
|
||||
function ip_login_register_success(string $ip): void
|
||||
{
|
||||
$now = time();
|
||||
$data = _ip_login_load();
|
||||
$st = $data[$ip] ?? ['fail' => [], 'ok' => [], 'block_until' => 0];
|
||||
_ip_login_prune($st, $now);
|
||||
$st['fail'] = []; // 成功登录重置失败计数(防爆破计数器归零)
|
||||
$st['ok'][] = $now; // 记录一次成功,纳入「30 分钟 5 次」上限
|
||||
_ip_login_prune($st, $now);
|
||||
$data[$ip] = $st;
|
||||
_ip_login_save($data);
|
||||
$data[$ip]['count']++;
|
||||
@file_put_contents($file, json_encode($data));
|
||||
}
|
||||
function ip_login_clear(string $ip): void
|
||||
{
|
||||
$data = _ip_login_load();
|
||||
$file = BASE_PATH . '/storage/login_ip.json';
|
||||
if (!is_file($file)) return;
|
||||
$data = json_decode(@file_get_contents($file), true) ?: [];
|
||||
unset($data[$ip]);
|
||||
_ip_login_save($data);
|
||||
@file_put_contents($file, json_encode($data));
|
||||
}
|
||||
|
||||
/* ---------- 通用 IP 级限速(可用于任意提交场景,如联系表单) ---------- */
|
||||
@@ -805,45 +745,3 @@ if (!function_exists('site_url')) {
|
||||
@file_put_contents($file, json_encode($data));
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('page_seo')) {
|
||||
/**
|
||||
* 取页面 SEO(标题/描述/关键词/OG/规范链接/收录开关)。
|
||||
* 优先读 page_seo 表;无记录或字段缺失时退回控制器传入的默认值。
|
||||
* @param string $key page_key(home/products/news/cases/about/contact...)
|
||||
* @param array $default 默认 SEO 数组(title/description/keywords/og_type)
|
||||
* @return array {title,description,keywords,og_type,og_image,canonical,noindex}
|
||||
*/
|
||||
function page_seo(string $key, array $default = []): array
|
||||
{
|
||||
$def = array_merge([
|
||||
'title' => '',
|
||||
'description' => '',
|
||||
'keywords' => '',
|
||||
'og_type' => 'website',
|
||||
'og_image' => '',
|
||||
'canonical' => '',
|
||||
'noindex' => 0,
|
||||
], $default);
|
||||
|
||||
try {
|
||||
$row = (new \App\Models\PageSeo())->getByKey($key);
|
||||
} catch (\Throwable $e) {
|
||||
$row = null;
|
||||
}
|
||||
|
||||
if (!$row) {
|
||||
return $def;
|
||||
}
|
||||
|
||||
return [
|
||||
'title' => $row['title'] ?? $def['title'],
|
||||
'description' => $row['description'] ?? $def['description'],
|
||||
'keywords' => $row['keywords'] ?? $def['keywords'],
|
||||
'og_type' => $row['og_type'] ?? $def['og_type'],
|
||||
'og_image' => $row['og_image'] ?? $def['og_image'],
|
||||
'canonical' => $row['canonical'] ?? $def['canonical'],
|
||||
'noindex' => $row['noindex'] ?? $def['noindex'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -252,21 +252,6 @@ class Installer
|
||||
private static function ensureColumns($pdo, array &$msgs): void
|
||||
{
|
||||
$map = [
|
||||
'pages' => [
|
||||
'mode' => "VARCHAR(16) NOT NULL DEFAULT 'fixed'",
|
||||
],
|
||||
'products' => [
|
||||
'mode' => "VARCHAR(16) NOT NULL DEFAULT 'fixed'",
|
||||
],
|
||||
'news' => [
|
||||
'mode' => "VARCHAR(16) NOT NULL DEFAULT 'fixed'",
|
||||
],
|
||||
'cases' => [
|
||||
'mode' => "VARCHAR(16) NOT NULL DEFAULT 'fixed'",
|
||||
],
|
||||
'categories' => [
|
||||
'mode' => "VARCHAR(16) NOT NULL DEFAULT 'fixed'",
|
||||
],
|
||||
'admin_users' => [
|
||||
'crm_role' => "VARCHAR(20) DEFAULT 'none'",
|
||||
'psi_role' => "VARCHAR(20) DEFAULT 'none'",
|
||||
|
||||
+1
-2
@@ -16,12 +16,11 @@ class Theme
|
||||
// 站点信息
|
||||
'site_name' => '酷冰甲 · 降温服',
|
||||
'site_slogan' => '科技降温 · 清凉一夏',
|
||||
'site_logo' => 'assets/img/logo.png',
|
||||
'site_logo' => '',
|
||||
'contact_phone' => '400-1783-998',
|
||||
'contact_email' => 'service@st-joyapparel.com',
|
||||
'contact_address'=> '江苏省苏州市工业园区',
|
||||
'icp' => '',
|
||||
'gongan' => '', // 公安备案号(网安备),如 京公网安备11010802012345号
|
||||
'seo_title' => '酷冰甲降温服 - 科技降温服装定制',
|
||||
'seo_keywords' => '降温服, cooling clothing, 降温工作服, 清凉服定制',
|
||||
'seo_description'=> '酷冰甲专注降温服研发与定制,采用相变蓄冷与循环水冷技术,为高温作业人群提供清凉解决方案。',
|
||||
|
||||
Reference in New Issue
Block a user