481 lines
25 KiB
PHP
481 lines
25 KiB
PHP
<?php
|
||
/**
|
||
* admin/visits.php —— 风控管理
|
||
* 访问日志查询(精简 / 完整两种模式 + IP / 关键词筛选 + 分页)与 IP 黑名单管理。
|
||
* 黑名单命中规则后前台与后台(含登录页)请求均被直接拒绝(与前台同一风控提示页)。
|
||
*/
|
||
$P = '../';
|
||
require_once __DIR__ . '/_guard.php';
|
||
require_once __DIR__ . '/_visits_lib.php';
|
||
|
||
$pdo = db();
|
||
|
||
function settings_set(string $key, string $value): void
|
||
{
|
||
$st = db()->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 'loginlog_clear':
|
||
login_log_clear();
|
||
$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) ---------- */
|
||
$visStat = vis_stat_all();
|
||
$statTotal = $visStat['total'];
|
||
$statIps = $visStat['ips'];
|
||
$statToday = $visStat['today'];
|
||
$statBan = $visStat['ban'];
|
||
|
||
/* ---------- 筛选参数(供表单回显)与访问记录查询 ---------- */
|
||
$mode = (string) ($_GET['mode'] ?? 'full');
|
||
if (!in_array($mode, ['full', 'compact'], true)) {
|
||
$mode = 'full';
|
||
}
|
||
$ipF = substr(trim((string) ($_GET['ip'] ?? '')), 0, 64);
|
||
$q = substr(trim((string) ($_GET['q'] ?? '')), 0, 100);
|
||
$page = max(1, (int) ($_GET['page'] ?? 1));
|
||
$perPage = 30;
|
||
$visAccess = vis_query_access($mode, $ipF, $q, $page, $perPage);
|
||
|
||
/* ---------- 当前访问者信息(用于黑名单自查提示) ---------- */
|
||
$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;
|
||
|
||
/* ---------- 登录信息:当前会话 / 历史成功登录 / 登录失败按「IP × 日期」聚合 ---------- */
|
||
$lgStat = login_log_stat();
|
||
$lgRecent = login_log_recent_ok(15);
|
||
$lgFailF = substr(trim((string) ($_GET['lf'] ?? '')), 0, 64);
|
||
$lgFailData = vis_query_loginfail($lgFailF, 200);
|
||
$curAdmin = (string) ($_SESSION['admin'] ?? '');
|
||
$curLoginAt = (string) ($_SESSION['login_at'] ?? '');
|
||
$curLoginIp = (string) ($_SESSION['login_ip'] ?? ($curIp !== '未知' ? $curIp : ''));
|
||
$curLoginUa = (string) ($_SESSION['login_ua'] ?? '');
|
||
$curLoginDevice = (string) ($_SESSION['login_device'] ?? '');
|
||
if ($curLoginDevice === '') {
|
||
$curLoginDevice = login_ua_brief((string) ($_SERVER['HTTP_USER_AGENT'] ?? ''));
|
||
}
|
||
|
||
/* ---------- 黑名单规则(直接读表展示,便于核对备注与来源) ---------- */
|
||
$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');
|
||
?>
|
||
<main class="page-main">
|
||
<div class="wrap page-body">
|
||
<h2 style="margin-bottom:6px">风控管理</h2>
|
||
<p class="tip">记录访客对本站的动态请求(含后台),完整模式逐条展示访问明细,精简模式按 IP 汇总次数;黑名单 / 自动风控对所有访问统一生效,命中即返回 403 风控提示页,后台与登录页不例外。</p>
|
||
|
||
<?php if ($msgText !== ''): ?>
|
||
<script>window.hpToastMsg = <?= json_encode(['kind' => $msgKind === 'ok' ? 'ok' : 'err', 'text' => $msgText], JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP); ?>;</script>
|
||
<?php endif; ?>
|
||
|
||
<!-- 概要统计 -->
|
||
<div class="fieldset-card">
|
||
<div class="fs-title">访问概况</div>
|
||
<div class="vis-stats">
|
||
<div class="vis-stat"><b><?= $statTotal ?></b><span>累计访问</span></div>
|
||
<div class="vis-stat"><b><?= $statIps ?></b><span>独立 IP</span></div>
|
||
<div class="vis-stat"><b><?= $statToday ?></b><span>今日访问</span></div>
|
||
<div class="vis-stat"><b><?= $statBan ?></b><span>黑名单规则</span></div>
|
||
</div>
|
||
<form method="post" data-confirm="确认清空全部访问日志?此操作不可恢复。" style="margin-top:14px">
|
||
<?= csrf_field() ?><input type="hidden" name="act" value="log_clear">
|
||
<button type="submit" class="btn btn-sm btn-danger">清空访问日志</button>
|
||
<span class="tip" style="margin-left:8px">日志按行追加写入 <code>data/vistors.log</code>(制表符分隔:时间 / IP / 请求方式 / 响应码 /
|
||
URL),完整保留不设上限;可自行归档或删除该文件。</span>
|
||
</form>
|
||
</div>
|
||
|
||
<!-- 登录信息(当前会话 + 历史成功登录) -->
|
||
<div class="fieldset-card">
|
||
<div class="fs-title">登录信息</div>
|
||
<div class="vis-stats">
|
||
<div class="vis-stat"><b><?= he($curAdmin) ?></b><span>当前用户</span></div>
|
||
<div class="vis-stat"><b
|
||
style="font-size:15px"><?= he($curLoginAt !== '' ? $curLoginAt : '本次会话未记录') ?></b><span>本次登录时间</span></div>
|
||
<div class="vis-stat"><b style="font-size:15px"><?= he($curLoginIp !== '' ? $curLoginIp : '未知') ?></b><span>本次登录
|
||
IP</span></div>
|
||
<div class="vis-stat"><b style="font-size:15px"><?= he($curLoginDevice) ?></b><span>本次登录设备</span></div>
|
||
</div>
|
||
<p class="tip" style="margin:12px 0 4px">历史登录记录(最近 <?= count($lgRecent) ?> 条成功登录;今日成功
|
||
<?= (int) $lgStat['ok_today'] ?> 次)
|
||
</p>
|
||
<div class="v-scroll">
|
||
<table class="v-tbl">
|
||
<thead>
|
||
<tr>
|
||
<th>登录时间</th>
|
||
<th>登录 IP</th>
|
||
<th>登录设备(浏览器)</th>
|
||
<th>用户名</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<?php if ($lgRecent): ?>
|
||
<?php foreach ($lgRecent as $r): ?>
|
||
<tr>
|
||
<td class="v-nowrap"><?= he((string) $r['created_at']) ?></td>
|
||
<td class="v-nowrap"><?= he((string) $r['ip']) ?></td>
|
||
<td><?= he(vis_device_text($r)) ?></td>
|
||
<td class="v-nowrap"><?= he((string) $r['username']) ?></td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
<?php else: ?>
|
||
<tr>
|
||
<td colspan="4"><span class="tip">暂无成功登录记录(本次登录为首次记录)。</span></td>
|
||
</tr>
|
||
<?php endif; ?>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 登录失败日志(按 IP × 日期聚合) -->
|
||
<div class="fieldset-card">
|
||
<div class="fs-title">登录失败日志</div>
|
||
<div class="vis-stats">
|
||
<div class="vis-stat"><b><?= (int) $lgStat['fail_today'] ?></b><span>今日失败次数</span></div>
|
||
<div class="vis-stat"><b><?= (int) $lgStat['fail_ips'] ?></b><span>失败涉及 IP</span></div>
|
||
<div class="vis-stat"><b><?= (int) $lgStat['ok_today'] ?></b><span>今日成功登录</span></div>
|
||
<div class="vis-stat"><b><?= (int) $lgStat['total'] ?></b><span>登录日志总数</span></div>
|
||
</div>
|
||
<form method="get" class="v-filter" style="margin-top:12px" data-vis-form="loginfail">
|
||
<input type="text" name="lf" value="<?= he($lgFailF) ?>" placeholder="按 IP 筛选,支持模糊(如 203.0)">
|
||
<button type="submit" class="btn btn-primary">筛选</button>
|
||
<a class="btn" href="visits.php" data-vis-section="loginfail" data-vis-page="">重置</a>
|
||
<span class="tip" style="align-self:center">按「IP × 日期」聚合:同一 IP 当天的失败次数与最近一次设备信息</span>
|
||
</form>
|
||
<div id="lfResults"><?= vis_render_loginfail($lgFailData) ?></div>
|
||
<form method="post" data-confirm="确认清空全部登录日志(成功与失败记录)?此操作不可恢复。" style="margin-top:12px">
|
||
<?= csrf_field() ?><input type="hidden" name="act" value="loginlog_clear">
|
||
<button type="submit" class="btn btn-sm btn-danger">清空登录日志</button>
|
||
<span class="tip" style="margin-left:8px">记录存于数据库 <code>login_log</code> 表(含成功与失败、时间 / 用户名 / IP / 设备)。</span>
|
||
</form>
|
||
</div>
|
||
|
||
<!-- 筛选 + 日志列表 -->
|
||
<div class="fieldset-card">
|
||
<div class="fs-title">访问记录</div>
|
||
<form method="get" class="v-filter" data-vis-form="access">
|
||
<select name="mode" title="展示模式"
|
||
onchange="if(this.form.requestSubmit){this.form.requestSubmit();}else{this.form.submit();}">
|
||
<option value="full" <?= $mode === 'full' ? ' selected' : '' ?>>完整模式(逐条明细)</option>
|
||
<option value="compact" <?= $mode === 'compact' ? ' selected' : '' ?>>精简模式(仅时间 / IP / 次数)</option>
|
||
</select>
|
||
<input type="text" name="ip" value="<?= he($ipF) ?>" placeholder="按 IP 筛选,支持模糊(如 203.0)">
|
||
<input type="text" name="q" class="w-s" value="<?= he($q) ?>" placeholder="关键词:地址 / 请求方式">
|
||
<button type="submit" class="btn btn-primary">筛选</button>
|
||
<a class="btn" href="visits.php" data-vis-section="access" data-vis-page="">重置</a>
|
||
</form>
|
||
|
||
<div id="visResults"><?= vis_render_access($visAccess) ?></div>
|
||
</div>
|
||
|
||
<!-- 自动风控规则 -->
|
||
<div class="fieldset-card">
|
||
<div class="fs-title">风控规则</div>
|
||
<form method="post">
|
||
<?= csrf_field() ?><input type="hidden" name="act" value="risk_save">
|
||
<div style="display:flex;flex-wrap:wrap;align-items:center;gap:10px;margin:9px 0">
|
||
<label class="seg-check" style="min-width:148px"><input type="checkbox" name="r_login_on" value="1"
|
||
<?= $rcfg['login_fail']['on'] ? ' checked' : '' ?>> 后台登录失败封禁</label>
|
||
<span class="tip">失败次数 ≥ <input type="number" name="r_login_n" min="1" max="1000000"
|
||
value="<?= (int) $rcfg['login_fail']['n'] ?>"
|
||
style="width:96px;padding:4px 8px;border:1px solid var(--line-2);border-radius:8px;background:var(--card);color:inherit">
|
||
次 → 永久封禁(建议 5-20)</span>
|
||
</div>
|
||
<div style="display:flex;flex-wrap:wrap;align-items:center;gap:10px;margin:9px 0">
|
||
<label class="seg-check" style="min-width:148px"><input type="checkbox" name="r_404_on" value="1"
|
||
<?= $rcfg['not_found']['on'] ? ' checked' : '' ?>> 前台 404 封禁</label>
|
||
<span class="tip">累计次数 ≥ <input type="number" name="r_404_n" min="1" max="1000000"
|
||
value="<?= (int) $rcfg['not_found']['n'] ?>"
|
||
style="width:96px;padding:4px 8px;border:1px solid var(--line-2);border-radius:8px;background:var(--card);color:inherit">
|
||
次 → 永久封禁(建议 50-300)</span>
|
||
</div>
|
||
<div style="display:flex;flex-wrap:wrap;align-items:center;gap:10px;margin:9px 0">
|
||
<label class="seg-check" style="min-width:148px"><input type="checkbox" name="r_rate_on" value="1"
|
||
<?= $rcfg['rate']['on'] ? ' checked' : '' ?>> 请求频率封禁</label>
|
||
<span class="tip">60 秒内 ≥ <input type="number" name="r_rate_n" min="1" max="1000000"
|
||
value="<?= (int) $rcfg['rate']['n'] ?>"
|
||
style="width:96px;padding:4px 8px;border:1px solid var(--line-2);border-radius:8px;background:var(--card);color:inherit">
|
||
次 → 永久封禁(多人共用出口 IP 时建议关闭)</span>
|
||
</div>
|
||
<p class="tip" style="margin:8px 0 6px">计数方式:404
|
||
与登录失败只要发生即累计(规则关闭也照常计数,便于后台观察);规则启用且达到阈值时自动封禁,保存后即时生效。自动封禁在黑名单中标注“风控自动”,可随时移除解封(解封时同步清除该 IP 计数)。</p>
|
||
<div style="display:flex;flex-wrap:wrap;align-items:center;gap:10px;margin:0 0 8px">
|
||
<span class="tip" style="min-width:148px">主提示(留空=默认)</span>
|
||
<input type="text" name="r_tip_main" maxlength="100" value="<?= he((string) $rcfg['_tip']['raw_main']) ?>"
|
||
placeholder="例:您触发了本站风控,请稍后再访问" title="黑名单命中时提示页的大号主文案"
|
||
style="flex:1 1 300px;min-width:260px;padding:6px 10px;border:1px solid var(--line-2);border-radius:8px;background:var(--card);color:inherit">
|
||
</div>
|
||
<div style="display:flex;flex-wrap:wrap;align-items:center;gap:10px;margin:0 0 12px">
|
||
<span class="tip" style="min-width:148px">次行说明(留空=默认)</span>
|
||
<input type="text" name="r_tip_sub" maxlength="200" value="<?= he((string) $rcfg['_tip']['raw_sub']) ?>"
|
||
placeholder="例:如为正常访问,请稍后重试;若频繁误判,请联系站点管理员处理。" title="主提示下方的次要说明文案"
|
||
style="flex:1 1 300px;min-width:260px;padding:6px 10px;border:1px solid var(--line-2);border-radius:8px;background:var(--card);color:inherit">
|
||
</div>
|
||
<button type="submit" class="btn btn-primary">保存风控规则</button>
|
||
</form>
|
||
<form method="post" data-confirm="确认清空全部风险计数?各 IP 将从 0 重新累计,不影响已封禁的黑名单。" style="margin-top:14px">
|
||
<?= csrf_field() ?><input type="hidden" name="act" value="risk_clear">
|
||
<button type="submit" class="btn btn-sm">清空全部风险计数</button>
|
||
<?php if ($curRisk): ?>
|
||
<span class="tip" style="margin-left:8px">当前访问 IP(<?= he($curIp) ?>)计数:登录失败 <?= (int) $curRisk['login_fail'] ?>
|
||
次
|
||
· 404 <?= (int) $curRisk['not_found'] ?> 次<?php if ($rcfg['rate']['on']): ?> · 本分钟请求
|
||
<?= (int) $curRisk['rate_count'] ?> 次<?php endif; ?></span>
|
||
<?php endif; ?>
|
||
</form>
|
||
</div>
|
||
|
||
<!-- IP 黑名单 -->
|
||
<div class="fieldset-card">
|
||
<div class="fs-title">黑名单管理</div>
|
||
<form method="post" class="v-filter">
|
||
<?= csrf_field() ?><input type="hidden" name="act" value="ban_add">
|
||
<input type="text" name="rule" required placeholder="如:203.0.113.7 / 203.0.113.0/24 / 203.0.113.*"
|
||
style="flex:1 1 260px;max-width:420px">
|
||
<input type="text" name="note" placeholder="备注(可选,如:恶意扫描器)" style="flex:1 1 220px;max-width:340px">
|
||
<button type="submit" class="btn btn-primary">加入黑名单</button>
|
||
</form>
|
||
<p class="tip" style="margin:10px 0 2px">
|
||
支持三种写法:单个 IP、CIDR 网段(如 <code>203.0.113.0/24</code>)、星号通配段(如 <code>203.0.113.*</code> 或
|
||
<code>192.168.*.*</code>);
|
||
目前仅按 IPv4 匹配。黑名单即时生效且不受「站长提示」开关影响,命中 IP 的<strong>前台与后台(含登录页)访问一律被拒绝</strong>。如需解除,请从未被封禁的网络进入后台移除该规则,或直接在
|
||
data/homepage.db 的 ip_blacklist 表中删除记录。
|
||
</p>
|
||
<div style="margin:6px 0 10px">
|
||
<?php if ($curHit): ?>
|
||
<span class="vis-warn-tip">当前访问 IP:<?= he($curIp) ?> ——
|
||
命中规则「<?= he($curHit['rule']) ?>」(<?= ($curHit['note'] !== '') ? he((string) $curHit['note']) : '无备注' ?>),全站(前台与后台)访问将被拦截</span>
|
||
<?php else: ?>
|
||
<span class="vis-ok-tip">当前访问 IP:<?= he($curIp) ?> —— 未命中任何黑名单规则</span>
|
||
<?php endif; ?>
|
||
</div>
|
||
|
||
<?php if ($banRows): ?>
|
||
<div style="display:flex;flex-wrap:wrap;align-items:center;gap:10px;margin:8px 0 4px">
|
||
<form method="post" id="blBatchForm" data-confirm="确认移除选中的黑名单规则?对应单 IP 风控计数将同步清除。">
|
||
<?= csrf_field() ?><input type="hidden" name="act" value="ban_batch">
|
||
<label class="tip"
|
||
style="display:inline-flex;align-items:center;gap:4px;cursor:pointer;margin:0 4px 0 0"><input
|
||
type="checkbox" id="blCheckAll"> 全选</label>
|
||
<button type="submit" class="btn btn-sm btn-danger" id="blBatchBtn" disabled>移除选中(0)</button>
|
||
</form>
|
||
<?php if ($banAutoCount > 0): ?>
|
||
<form method="post" data-confirm="确认一键移除全部风控自动封禁(共 <?= $banAutoCount ?> 条)?相关 IP 风控计数将同步清除。">
|
||
<?= csrf_field() ?><input type="hidden" name="act" value="ban_clear_auto">
|
||
<button type="submit" class="btn btn-sm">一键移除风控自动(<?= $banAutoCount ?>)</button>
|
||
</form>
|
||
<?php endif; ?>
|
||
<form method="post" data-confirm="确认清空全部黑名单?全部风险计数将一并清零。">
|
||
<?= csrf_field() ?><input type="hidden" name="act" value="ban_clear_all">
|
||
<button type="submit" class="btn btn-sm btn-danger">清空全部</button>
|
||
</form>
|
||
</div>
|
||
<?php foreach ($banRows as $b): ?>
|
||
<div class="bl-row">
|
||
<label class="tip" style="display:inline-flex;align-items:center;margin:0" title="勾选后可在上方批量移除"><input
|
||
type="checkbox" form="blBatchForm" class="js-bl-check" name="bid[]" value="<?= (int) $b['id'] ?>"></label>
|
||
<code class="bl-rule"><?= he((string) $b['rule']) ?></code>
|
||
<?php if (((string) ($b['source'] ?? 'manual')) === 'auto'): ?>
|
||
<span class="v-tag warn" style="min-width:0">风控自动</span>
|
||
<?php else: ?>
|
||
<span class="v-tag info" style="min-width:0">手动</span>
|
||
<?php endif; ?>
|
||
<span
|
||
class="bl-note"><?= $b['note'] !== '' ? he((string) $b['note']) : '<span class="tip">(无备注)</span>' ?></span>
|
||
<span class="bl-time"><?= he((string) $b['created_at']) ?></span>
|
||
<form method="post" data-confirm="确认移除黑名单规则「<?= he((string) $b['rule']) ?>」?">
|
||
<?= csrf_field() ?><input type="hidden" name="act" value="ban_del"><input type="hidden" name="id"
|
||
value="<?= (int) $b['id'] ?>">
|
||
<button type="submit" class="btn btn-sm btn-danger">移除</button>
|
||
</form>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
<?php else: ?>
|
||
<p class="tip" style="margin:6px 0 0">尚未配置黑名单规则。启用上方「自动风控」后,异常 IP 会自动记录到这里;也可在下方手动添加规则。</p>
|
||
<?php endif; ?>
|
||
</div>
|
||
</div>
|
||
</main>
|
||
|
||
<?php layout_theme_fab(); ?>
|
||
<script src="../assets/js/common.js"></script>
|
||
<script>
|
||
(function () {
|
||
var all = document.getElementById('blCheckAll');
|
||
var btn = document.getElementById('blBatchBtn');
|
||
var boxes = Array.prototype.slice.call(document.querySelectorAll('.js-bl-check'));
|
||
if (!all || !btn || !boxes.length) return;
|
||
function refresh() {
|
||
var n = boxes.filter(function (c) { return c.checked; }).length;
|
||
btn.disabled = n === 0;
|
||
btn.textContent = '移除选中(' + n + ')';
|
||
all.checked = n > 0 && n === boxes.length;
|
||
}
|
||
all.addEventListener('change', function () {
|
||
boxes.forEach(function (c) { c.checked = all.checked; });
|
||
refresh();
|
||
});
|
||
boxes.forEach(function (c) { c.addEventListener('change', refresh); });
|
||
})();
|
||
</script>
|
||
</body>
|
||
|
||
</html>
|