prepare('INSERT INTO settings (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value'); $st->execute([$key, $value]); } /* 批量删除黑名单(按 id 列表),同步清除单 IP 规则的风控计数;返回删除条数 */ function bl_delete_ids(PDO $pdo, array $ids): int { $ids = array_values(array_unique(array_filter(array_map('intval', $ids), function ($v) { return $v > 0; }))); if (!$ids) return 0; $ph = implode(',', array_fill(0, count($ids), '?')); $st = $pdo->prepare('SELECT rule FROM ip_blacklist WHERE id IN (' . $ph . ')'); $st->execute($ids); $rules = $st->fetchAll(); $pdo->prepare('DELETE FROM ip_blacklist WHERE id IN (' . $ph . ')')->execute($ids); foreach ($rules as $r) { if (filter_var((string)$r['rule'], FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { risk_clear_for_ip((string)$r['rule']); } } return count($rules); } /* ---------- POST:黑名单增删 / 清空日志 / 风控规则 ---------- */ $msgKind = ''; $msgText = ''; $banChanged = false; if ($_SERVER['REQUEST_METHOD'] === 'POST') { if (!csrf_verify()) { $msgKind = 'err'; $msgText = '安全校验失败,请刷新页面重试。'; } else { $act = (string)($_POST['act'] ?? ''); switch ($act) { case 'ban_add': $rule = trim((string)($_POST['rule'] ?? '')); $note = trim((string)($_POST['note'] ?? '')); if ($rule === '') { $msgKind = 'err'; $msgText = '请填写要拦截的 IP / 网段。'; break; } $parsed = blacklist_parse_rule($rule); if (!$parsed[0]) { $msgKind = 'err'; $msgText = $parsed[3]; break; } $st = $pdo->prepare('INSERT INTO ip_blacklist (rule, note, created_at, source) VALUES (?,?,?,?)'); $st->execute([$rule, $note, date('Y-m-d H:i:s'), 'manual']); $banChanged = true; $msgKind = 'ok'; $msgText = '黑名单规则已添加,匹配的访问将立即被拒绝。'; break; case 'ban_del': $id = max(0, (int)($_POST['id'] ?? 0)); if ($id > 0) { $bs = $pdo->prepare('SELECT rule FROM ip_blacklist WHERE id = ? LIMIT 1'); $bs->execute([$id]); $brow = $bs->fetch(); $pdo->prepare('DELETE FROM ip_blacklist WHERE id = ?')->execute([$id]); // 解封单 IP 时同步清除其风控计数,避免“刚解封又被历史计数封回” if ($brow && filter_var((string)$brow['rule'], FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { risk_clear_for_ip((string)$brow['rule']); } $banChanged = true; $msgKind = 'ok'; $msgText = '黑名单规则已移除。'; } else { $msgKind = 'err'; $msgText = '无效的黑名单记录。'; } break; case 'log_clear': @file_put_contents(ACCESS_LOG_FILE, '', LOCK_EX); $msgKind = 'ok'; $msgText = '访问日志已清空(data/vistors.log)。'; break; case 'risk_save': $clip = function ($v, $d) { $raw = trim((string)($v ?? '')); $x = ($raw === '') ? (int)$d : (int)$raw; return max(1, min(1000000, $x)); }; settings_set('risk_login_on', isset($_POST['r_login_on']) ? '1' : '0'); settings_set('risk_login_n', (string)$clip($_POST['r_login_n'] ?? null, 10)); settings_set('risk_404_on', isset($_POST['r_404_on']) ? '1' : '0'); settings_set('risk_404_n', (string)$clip($_POST['r_404_n'] ?? null, 100)); settings_set('risk_rate_on', isset($_POST['r_rate_on']) ? '1' : '0'); settings_set('risk_rate_n', (string)$clip($_POST['r_rate_n'] ?? null, 120)); settings_set('risk_tip_main', trim((string)($_POST['r_tip_main'] ?? ''))); settings_set('risk_tip_sub', trim((string)($_POST['r_tip_sub'] ?? ''))); $msgKind = 'ok'; $msgText = '风控规则已保存并即时生效。'; break; case 'risk_clear': risk_clear_all(); $msgKind = 'ok'; $msgText = '全部风险计数已清空(黑名单本身不受影响)。'; break; case 'ban_batch': $n = bl_delete_ids($pdo, (array)($_POST['bid'] ?? [])); $banChanged = true; $msgKind = $n > 0 ? 'ok' : 'err'; $msgText = $n > 0 ? '已批量移除 ' . $n . ' 条黑名单规则(对应单 IP 计数已清除)。' : '未勾选任何需要移除的规则。'; break; case 'ban_clear_auto': $rows = $pdo->query("SELECT rule FROM ip_blacklist WHERE source = 'auto'")->fetchAll(); $cnt = count($rows); $pdo->exec("DELETE FROM ip_blacklist WHERE source = 'auto'"); foreach ($rows as $r) { if (filter_var((string)$r['rule'], FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { risk_clear_for_ip((string)$r['rule']); } } blacklist_rules(true); $banChanged = true; $msgKind = 'ok'; $msgText = '已一键移除全部风控自动封禁(共 ' . $cnt . ' 条),相关 IP 风控计数已同步清除。'; break; case 'ban_clear_all': $pdo->exec('DELETE FROM ip_blacklist'); risk_clear_all(); blacklist_rules(true); $banChanged = true; $msgKind = 'ok'; $msgText = '已清空全部黑名单,风险计数一并清零。'; break; default: $msgKind = 'err'; $msgText = '未知操作。'; } } } /* ---------- 概要统计(数据来源:data/vistors.log) ---------- */ $entries = visitors_entries(); $statTotal = count($entries); $ipSet = []; foreach ($entries as $e) { $ipSet[$e['ip']] = 1; } $statIps = count($ipSet); $statToday = 0; $todayStart = date('Y-m-d 00:00:00'); foreach ($entries as $e) { if ($e['created_at'] >= $todayStart) { $statToday++; } } function vis_scalar(PDO $pdo, string $sql): int { $v = $pdo->query($sql)->fetchColumn(); return (int)$v; } $statBan = vis_scalar($pdo, 'SELECT COUNT(*) FROM ip_blacklist'); /* ---------- 筛选与分页 ---------- */ $mode = (string)($_GET['mode'] ?? 'full'); if (!in_array($mode, ['full', 'compact'], true)) { $mode = 'full'; } $ipF = trim((string)($_GET['ip'] ?? '')); $q = trim((string)($_GET['q'] ?? '')); $ipF = substr($ipF, 0, 64); $q = substr($q, 0, 100); $page = max(1, (int)($_GET['page'] ?? 1)); $perPage = 30; // entries 已按新→旧排列,内存中过滤 / 聚合后分页 $logs = []; $total = 0; $totalPages = 1; if ($mode === 'compact') { // 精简模式:按 IP 聚合(最近访问时间 / IP / 访问次数),首次出现即该 IP 最新一条 $grp = []; $order = []; foreach ($entries as $e) { if ($ipF !== '' && stripos($e['ip'], $ipF) === false) continue; if ($q !== '' && stripos($e['url'], $q) === false && stripos($e['method'], $q) === false) continue; if (!isset($grp[$e['ip']])) { $grp[$e['ip']] = ['ip' => $e['ip'], 'last_at' => $e['created_at'], 'cnt' => 0]; $order[] = $e['ip']; } $grp[$e['ip']]['cnt']++; } $all = []; foreach ($order as $ip) { $all[] = $grp[$ip]; } $total = count($all); $totalPages = max(1, (int)ceil($total / $perPage)); if ($page > $totalPages) { $page = $totalPages; } $off = ($page - 1) * $perPage; $logs = array_slice($all, $off, $perPage); } else { // 完整模式:逐条明细 $all = []; foreach ($entries as $e) { if ($ipF !== '' && stripos($e['ip'], $ipF) === false) continue; if ($q !== '' && stripos($e['url'], $q) === false && stripos($e['method'], $q) === false) continue; $all[] = $e; } $total = count($all); $totalPages = max(1, (int)ceil($total / $perPage)); if ($page > $totalPages) { $page = $totalPages; } $off = ($page - 1) * $perPage; $logs = array_slice($all, $off, $perPage); } /* ---------- 链接工具(保留当前筛选参数) ---------- */ function vis_url(array $ov): string { $p = $_GET; foreach ($ov as $k => $v) { if ($v === null || $v === '') { unset($p[$k]); } else { $p[$k] = $v; } } if (!$p) { return 'visits.php'; } return 'visits.php?' . http_build_query($p); } /* ---------- 当前访问者信息(用于黑名单自查提示) ---------- */ $curIp = client_ip(); if ($curIp === '') { $curIp = '未知'; } $curHit = null; if ($curIp !== '未知') { if ($banChanged) { blacklist_rules(true); } $curHit = blacklist_ip_hit($curIp); } /* ---------- 自动风控规则配置与当前 IP 计数 ---------- */ $rcfg = risk_rule_cfg(); $curRisk = ($curIp !== '未知') ? risk_row($curIp) : null; /* ---------- 黑名单规则(直接读表展示,便于核对备注与来源) ---------- */ $banRows = $pdo->query('SELECT id, rule, note, created_at, source FROM ip_blacklist ORDER BY id DESC')->fetchAll(); $banAutoCount = 0; foreach ($banRows as $b) { if (((string)($b['source'] ?? 'manual')) === 'auto') $banAutoCount++; } layout_head('访问信息'); admin_topbar('visits'); ?>

访问信息

记录访客对本站的动态请求(含后台),完整模式逐条展示访问明细,精简模式按 IP 汇总次数;黑名单 / 自动风控对所有访问统一生效,命中即返回 403 风控提示页,后台与登录页不例外。

访问概况
累计访问
独立 IP
今日访问
黑名单规则
日志按行追加写入 data/vistors.log(制表符分隔:时间 / IP / 请求方式 / 响应码 / URL),完整保留不设上限;可自行归档或删除该文件。
访问记录
重置
共命中 ; 当前第 / 页,每页 条。
最近访问时间IP 地址访问次数
访问时间IP 地址请求方式访问 URL响应码
= 200 && $code < 300) ? 'good' : (($code >= 300 && $code < 400) ? 'info' : (($code >= 400 && $code < 500) ? 'warn' : 'bad')); ?>
1): ?>
1): ?>← 上一页 1) echo ''; for ($p = $start; $p <= $end; $p++) { if ($p === $page) { echo '' . $p . ''; } else { echo '' . $p . ''; } } if ($end < $totalPages) echo ''; ?> 下一页 →
自动风控规则(达到阈值自动加入黑名单;命中前台与后台均返回 403 风控提示页,提示文案可在下方自定义)
失败次数 ≥ 次 → 永久封禁(建议 5-20)
累计次数 ≥ 次 → 永久封禁(建议 50-300)
60 秒内 ≥ 次 → 永久封禁(多人共用出口 IP 时建议关闭)

计数方式:404 与登录失败只要发生即累计(规则关闭也照常计数,便于后台观察);规则启用且达到阈值时自动封禁,保存后即时生效。自动封禁在黑名单中标注“风控自动”,可随时移除解封(解封时同步清除该 IP 计数)。

主提示(留空=默认)
次行说明(留空=默认)
当前访问 IP()计数:登录失败 次 · 404 · 本分钟请求
IP 黑名单(全站拦截前台与后台,命中统一返回 403 风控提示页)

支持三种写法:单个 IP、CIDR 网段(如 203.0.113.0/24)、星号通配段(如 203.0.113.*192.168.*.*); 目前仅按 IPv4 匹配。黑名单即时生效且不受「站长提示」开关影响,命中 IP 的前台与后台(含登录页)访问一律被拒绝。如需解除,请从未被封禁的网络进入后台移除该规则,或直接在 data/homepage.db 的 ip_blacklist 表中删除记录。

当前访问 IP: —— 命中规则「」(),全站(前台与后台)访问将被拦截 当前访问 IP: —— 未命中任何黑名单规则
0): ?>
风控自动 手动 (无备注)' ?>

尚未配置黑名单规则。启用上方「自动风控」后,异常 IP 会自动记录到这里;也可在下方手动添加规则。