在经营 WordPress 站点的过程中,最头疼的不是没流量,而是那些没完没了的自动化扫描器、CC 攻击和寻找 .env 泄露的机器人。
为了实现毫秒级响应和深度行为追踪,我开发了这款名为 Site Behavior Auditor (SBA) 的轻量级插件。它不依赖第三方 API,完全运行在本地,支持 IPv6 归属地识别,并具备三层递进防御体系。
一、 核心防御原理
SBA 的设计哲学是:在 WordPress 加载逻辑的最前端拦截非法请求。
- 特征识别 (Fingerprinting):利用
HTTP_USER_AGENT识别已知的扫描工具(如 sqlmap, nmap)。 - 路径诱捕 (Path Trapping):监控敏感路径(如
.git,.env),一旦触碰,立即判定为恶意攻击。 - 流量整形 (Rate Limiting):基于本地数据库记录,计算单 IP 每分钟的请求频率,自动阻断 CC 行为。
- 身份闭环 (The Gate):重命名登录入口并配合用户名白名单,让站长在防御开启时依然能“横着走”。
二、 核心功能特性
- 实时访客轨迹:异步加载,精确记录每一个 IP 的访问路径、时间及 PV。
- IPv4/v6 双栈归属地:支持本地导入 7 列格式 IP 库,毫秒级解析地理位置,不泄露访客隐私。
- 30天趋势图表:双曲线展示 UV 和 PV 的变化趋势。
- 拦截日志系统:详细记录每一次拦截的时间、原因以及对方试图访问的目标。
- 50天审计详表:深度分析历史访问数据与平均访问深度。
三、 插件全量源代码
使用说明:在wp-content/plugins目录中新建一个文件夹
site-behavior-auditor,在其中创建site-behavior-auditor.php,复制以下全部代码。
<?php
/*
Plugin Name: Site Behavior Auditor (SBA)
Description: v1.0 正式版:全行为审计、路径拦截、CC防御、IPv6归属地解析、30天趋势图。
Version: 1.0
Author: Stone
*/
if (!defined('ABSPATH')) exit;
/* ================= 1. 数据库持久化 ================= */
register_activation_hook(__FILE__, function() {
global $wpdb; $col = $wpdb->get_charset_collate();
$sql = [
"CREATE TABLE IF NOT EXISTS {$wpdb->prefix}dis_stats (id BIGINT AUTO_INCREMENT PRIMARY KEY, ip VARCHAR(45), url TEXT, visit_date DATE, visit_hour TINYINT, pv INT DEFAULT 1, last_visit TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY ip_date_url (ip, visit_date, url(191)), INDEX idx_lookup (visit_date, last_visit DESC)) $col;",
"CREATE TABLE IF NOT EXISTS {$wpdb->prefix}sba_ip_data (id int(11) NOT NULL AUTO_INCREMENT, ip_type tinyint(1) DEFAULT 4, start_bin varbinary(16) NOT NULL, end_bin varbinary(16) NOT NULL, addr varchar(255) NOT NULL, PRIMARY KEY (id), KEY range_idx (ip_type, start_bin, end_bin)) $col;",
"CREATE TABLE IF NOT EXISTS {$wpdb->prefix}sba_blocked_log (id BIGINT AUTO_INCREMENT PRIMARY KEY, ip VARCHAR(45), reason VARCHAR(100), target_url TEXT, block_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP) $col;"
];
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
foreach($sql as $s) dbDelta($s);
});
/* ================= 2. 核心拦截引擎 ================= */
function sba_get_ip() {
$ip = $_SERVER['HTTP_CF_CONNECTING_IP'] ?? $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'];
return filter_var(trim(explode(',', $ip)[0]), FILTER_VALIDATE_IP) ?: '0.0.0.0';
}
function sba_get_opt($k, $d='') {
$o = get_option('sba_settings');
return (isset($o[$k]) && $o[$k] !== '') ? $o[$k] : $d;
}
function sba_execute_block($reason) {
// 权限与白名单校验:确保管理员及白名单用户不被封锁
$u_white = array_filter(array_map('trim', explode(',', sba_get_opt('user_whitelist', ''))));
$user = wp_get_current_user();
if ($user->exists() && in_array($user->user_login, $u_white)) return;
if (current_user_can('manage_options')) return;
$ip = sba_get_ip();
$ip_white = array_filter(array_map('trim', explode(',', sba_get_opt('ip_whitelist', ''))));
if (in_array($ip, $ip_white)) return;
global $wpdb;
$wpdb->insert($wpdb->prefix . "sba_blocked_log", ['ip' => $ip, 'reason' => $reason, 'target_url' => $_SERVER['REQUEST_URI']]);
$target = sba_get_opt('block_target_url', '');
if (!empty($target) && filter_var($target, FILTER_VALIDATE_URL)) { wp_redirect($target); exit; }
wp_die("🛡️ SBA 安全拦截:$reason", "Security Block", 403);
}
add_action('init', function() {
if (is_admin()) return;
$ip = sba_get_ip(); $uri = strtolower($_SERVER['REQUEST_URI']); $ua = $_SERVER['HTTP_USER_AGENT'] ?? '';
// 1. 指纹拦截
$scan_tools = ['sqlmap','nmap','dirbuster','nikto','zgrab','python-requests','go-http-client','java/','curl/','wget','masscan'];
foreach($scan_tools as $tool) { if (stripos($ua, $tool) !== false) sba_execute_block("自动化扫描器: $tool"); }
// 2. 路径拦截
$fixed_evil = ['/.env', '/.git', '/.sql', '/.ssh', '/wp-config.php.bak', '/phpinfo.php', '/config.php.swp', '/.vscode', '/xmlrpc.php'];
$custom_evil = array_filter(array_map('trim', explode(',', sba_get_opt('evil_paths', ''))));
$all_evil = array_unique(array_merge($fixed_evil, $custom_evil));
foreach ($all_evil as $path) { if (!empty($path) && strpos($uri, $path) !== false) sba_execute_block("非法路径探测: $path"); }
// 3. 频率拦截 (CC防御)
$limit = (int)sba_get_opt('auto_block_limit', 0);
if ($limit > 0) {
global $wpdb;
$count = $wpdb->get_var($wpdb->prepare("SELECT SUM(pv) FROM {$wpdb->prefix}dis_stats WHERE ip = %s AND last_visit > DATE_SUB(NOW(), INTERVAL 1 MINUTE)", $ip));
if ($count > $limit) sba_execute_block("频率超限 (CC风险)");
}
// 4. 后台 Gate 锁
$slug = sba_get_opt('login_slug', '');
if (!empty($slug) && strpos($uri, 'wp-login.php') !== false && (!isset($_GET['gate']) || $_GET['gate'] !== $slug)) sba_execute_block("Gate 钥匙错误");
global $wpdb;
$wpdb->query($wpdb->prepare("INSERT INTO {$wpdb->prefix}dis_stats (ip, url, visit_date, visit_hour, pv) VALUES (%s, %s, CURDATE(), %d, 1) ON DUPLICATE KEY UPDATE pv = pv + 1, last_visit = NOW()", $ip, $_SERVER['REQUEST_URI'], date('G')));
});
/* ================= 3. AJAX 异步处理 ================= */
add_action('wp_ajax_sba_get_geo', function() {
global $wpdb; $ips = (array)$_POST['ips']; $cache = get_option('sba_geo_cache', []); $results = [];
foreach($ips as $ip) {
if (isset($cache[$ip])) { $results[$ip] = $cache[$ip]; continue; }
$loc = ""; $ip_bin = @inet_pton($ip);
if ($ip_bin) {
$type = (strpos($ip, ':') !== false) ? 6 : 4;
$loc = $wpdb->get_var($wpdb->prepare("SELECT addr FROM {$wpdb->prefix}sba_ip_data WHERE ip_type = %d AND %s BETWEEN start_bin AND end_bin LIMIT 1", $type, $ip_bin));
}
$final = ($loc ?: "未知位置"); $results[$ip] = $final; $cache[$ip] = $final;
}
update_option('sba_geo_cache', $cache, false); wp_send_json_success($results);
});
add_action('wp_ajax_sba_load_tracks', function() {
global $wpdb; $p = intval($_POST['page'] ?? 1); $per = 50; $off = ($p - 1) * $per;
$total = $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}dis_stats WHERE visit_date = CURDATE()");
$pages = max(1, ceil($total / $per));
$rows = $wpdb->get_results($wpdb->prepare("SELECT ip, url, pv, last_visit FROM {$wpdb->prefix}dis_stats WHERE visit_date = CURDATE() ORDER BY last_visit DESC LIMIT %d, %d", $off, $per));
$html = "";
if($rows) { foreach($rows as $r) {
$html .= "<tr><td>".date('H:i',strtotime($r->last_visit))."</td><td><code>".$r->ip."</code></td><td><small class='geo-tag' data-ip='".$r->ip."'>解析中...</small></td><td><div class='sba-cell-wrap'><small>".esc_html($r->url)."</small></div></td><td><b>".$r->pv."</b></td></tr>";
} }
wp_send_json_success(['html' => $html, 'pages' => $pages, 'total' => $total]);
});
/* ================= 4. 后台 UI 看板 ================= */
add_action('admin_menu', function() {
add_menu_page('全行为审计', '全行为审计', 'manage_options', 'sba_audit', 'sba_render_dashboard', 'dashicons-shield-alt');
add_submenu_page('sba_audit', '防御设置', '防御设置', 'manage_options', 'sba_settings', 'sba_render_settings');
});
function sba_render_dashboard() {
global $wpdb;
$online = $wpdb->get_var("SELECT COUNT(DISTINCT ip) FROM {$wpdb->prefix}dis_stats WHERE last_visit > DATE_SUB(NOW(), INTERVAL 5 MINUTE)");
$today = $wpdb->get_row("SELECT COUNT(DISTINCT ip) as uv, SUM(pv) as pv FROM {$wpdb->prefix}dis_stats WHERE visit_date = CURDATE()");
$history_50 = $wpdb->get_results("SELECT visit_date, COUNT(DISTINCT ip) as uv, SUM(pv) as pv FROM {$wpdb->prefix}dis_stats GROUP BY visit_date ORDER BY visit_date DESC LIMIT 50", OBJECT_K);
$chart_labels = []; $chart_uv = []; $chart_pv = [];
for ($i = 29; $i >= 0; $i--) {
$date = date('Y-m-d', strtotime("-$i days"));
$chart_labels[] = $date;
$chart_uv[] = $history_50[$date]->uv ?? 0;
$chart_pv[] = $history_50[$date]->pv ?? 0;
}
$blocks = $wpdb->get_results("SELECT * FROM {$wpdb->prefix}sba_blocked_log ORDER BY block_time DESC LIMIT 15");
?>
<style>
.sba-wrap { max-width: 1400px; margin-top: 15px; font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif; }
.sba-card { background:#fff; padding:20px; border-radius:12px; margin-bottom:20px; box-shadow:0 4px 15px rgba(0,0,0,0.05); }
.sba-grid { display:grid; grid-template-columns: 1fr 1fr; gap:20px; }
@media (max-width: 1000px) { .sba-grid { grid-template-columns: 1fr; } }
.sba-scroll-x { width: 100%; overflow-x: auto; border: 1px solid #eee; border-radius:8px; }
.sba-table { width: 100%; min-width: 850px; border-collapse: collapse; table-layout: fixed; }
.sba-table th, .sba-table td { text-align: left; padding: 12px 10px; border-bottom: 1px solid #f9f9f9; }
.sba-cell-wrap { white-space: normal; word-break: break-all; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; line-height: 1.4; font-size:12px; }
.stat-val { font-size: 26px; font-weight: bold; display: block; margin-top: 5px; }
</style>
<div class="wrap sba-wrap">
<h2>🚀 SBA 站点审计概览 v1.0</h2>
<div style="display:flex; gap:15px; margin-bottom:20px; flex-wrap:wrap;">
<div class="sba-card" style="flex:1; border-left:4px solid #46b450;">当前在线: <span class="stat-val" style="color:#46b450;"><?php echo $online?:0; ?></span></div>
<div class="sba-card" style="flex:1; border-left:4px solid #2271b1;">今日 UV: <span class="stat-val" style="color:#2271b1;"><?php echo $today->uv?:0; ?></span></div>
<div class="sba-card" style="flex:1; border-left:4px solid #4fc3f7;">今日 PV: <span class="stat-val" style="color:#4fc3f7;"><?php echo $today->pv?:0; ?></span></div>
</div>
<div class="sba-grid">
<div class="sba-card"><h3>📈 30天访问趋势</h3><div style="height:250px;"><canvas id="sbaChart10"></canvas></div></div>
<div class="sba-card"><h3>📊 50天审计详表</h3>
<div class="sba-scroll-x" style="height:250px;"><table class="sba-table" style="min-width:400px;">
<thead><tr><th>日期</th><th>UV (人)</th><th>PV (次)</th><th>深度</th></tr></thead>
<tbody><?php for ($j = 0; $j < 50; $j++): $d = date('Y-m-d', strtotime("-$j days")); $u = $history_50[$d]->uv ?? 0; $p = $history_50[$d]->pv ?? 0; ?>
<tr><td><b><?php echo $d; ?></b></td><td><?php echo $u; ?></td><td><?php echo $p; ?></td><td><code><?php echo round($p/max(1,$u), 1); ?></code></td></tr><?php endfor; ?></tbody>
</table></div>
</div>
</div>
<div class="sba-card">
<h3>👣 实时访客轨迹</h3>
<div class="sba-scroll-x"><table class="sba-table">
<thead><tr><th width="80">时间</th><th width="150">IP 地址</th><th width="200">归属地</th><th>访问路径</th><th width="60">PV</th></tr></thead>
<tbody id="track-body"></tbody>
</table></div>
<div style="margin-top:15px; display:flex; justify-content: space-between;">
<div>总计: <b id="total-rows">0</b></div>
<div><button id="prev-page" class="button">上页</button> 第 <b id="current-page">1</b> / <b id="total-pages">1</b> 页 <button id="next-page" class="button">下页</button></div>
</div>
</div>
<div class="sba-card" style="border-top:3px solid #d63638;">
<h3>🚫 拦截日志 (Security Log)</h3>
<div class="sba-scroll-x"><table class="sba-table">
<thead><tr><th width="100">时间</th><th width="150">拦截 IP</th><th>拦截原因与目标</th></tr></thead>
<tbody><?php foreach($blocks as $b): ?><tr><td><?php echo date('m-d H:i', strtotime($b->block_time)); ?></td><td><code><?php echo $b->ip; ?></code></td><td class="sba-cell-wrap" style="color:#d63638;"><?php echo $b->reason; ?> ⚡ <?php echo esc_html($b->target_url); ?></td></tr><?php endforeach; ?></tbody>
</table></div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
new Chart(document.getElementById('sbaChart10'), {
type:'line', data:{
labels:<?php echo json_encode($chart_labels); ?>,
datasets:[
{label:'UV', data:<?php echo json_encode($chart_uv); ?>, borderColor:'#2271b1', tension:0.1, fill:false},
{label:'PV', data:<?php echo json_encode($chart_pv); ?>, borderColor:'#4fc3f7', tension:0.1, fill:false}
]
}, options:{maintainAspectRatio:false}
});
let curP = 1, maxP = 1;
const loadT = (p) => {
fetch(ajaxurl, { method: 'POST', body: new URLSearchParams({action:'sba_load_tracks', page:p}) }).then(r => r.json()).then(res => {
if(res.success) { document.getElementById('track-body').innerHTML = res.data.html; curP = p; maxP = res.data.pages; document.getElementById('current-page').innerText = p; document.getElementById('total-pages').innerText = maxP; document.getElementById('total-rows').innerText = res.data.total; processGeos(); }
});
};
async function processGeos() {
const badges = Array.from(document.querySelectorAll('.geo-tag')).filter(b => b.innerText === '解析中...');
for (let i = 0; i < badges.length; i += 5) {
const chunk = badges.slice(i, i + 5);
const fd = new FormData(); fd.append('action', 'sba_get_geo'); chunk.forEach(b => fd.append('ips[]', b.dataset.ip));
fetch(ajaxurl, { method: 'POST', body: fd }).then(r => r.json()).then(j => { if(j.success) chunk.forEach(b => b.innerText = j.data[b.dataset.ip]); });
}
}
document.getElementById('prev-page').onclick = () => { if(curP > 1) loadT(curP - 1); };
document.getElementById('next-page').onclick = () => { if(curP < maxP) loadT(curP + 1); };
loadT(1);
</script>
<?php
}
/* ================= 5. 设置页面 ================= */
function sba_render_settings() {
$opts = get_option('sba_settings');
global $wpdb;
$v4 = $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}sba_ip_data WHERE ip_type = 4");
$v6 = $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}sba_ip_data WHERE ip_type = 6");
?>
<div class="wrap sba-wrap">
<h1>🛠️ SBA 设置与说明手册</h1>
<div class="sba-card" style="background:#fffbe6; border-left:5px solid #faad14;">
<h3>📖 使用说明 (必读)</h3>
<p>1. <b>用户名白名单</b>:填入管理员用户名,登录状态下免除所有拦截,防止站长误杀。</p>
<p>2. <b>Gate 钥匙</b>:若设为 <code>xyz</code>,则登录地址强制变为 <code>wp-login.php?gate=xyz</code>。</p>
<p>3. <b>IP 库导入</b>:上传 7 列格式的 TXT 文件,系统自动区分 IPv4 与 IPv6 载入。</p>
</div>
<form method="post" action="options.php">
<?php settings_fields('sba_settings_group'); register_setting('sba_settings_group', 'sba_settings'); ?>
<div class="sba-grid">
<div class="sba-card">
<h3>✅ 信任通道</h3>
<table class="form-table">
<tr><th>用户名白名单</th><td><input type="text" name="sba_settings[user_whitelist]" value="<?php echo esc_attr($opts['user_whitelist'] ?? ''); ?>" placeholder="Stone" class="regular-text" /><br><small>登录此用户时,系统自动信任。</small></td></tr>
<tr><th>IP 白名单</th><td><textarea name="sba_settings[ip_whitelist]" rows="3" style="width:100%"><?php echo esc_textarea($opts['ip_whitelist'] ?? ''); ?></textarea></td></tr>
</table>
</div>
<div class="sba-card">
<h3>🚫 防御配置</h3>
<table class="form-table">
<tr><th>CC 封禁阈值</th><td><input type="number" name="sba_settings[auto_block_limit]" value="<?php echo esc_attr($opts['auto_block_limit'] ?? '60'); ?>" /> 次/分</td></tr>
<tr><th>Gate 钥匙</th><td><input type="text" name="sba_settings[login_slug]" value="<?php echo esc_attr($opts['login_slug'] ?? ''); ?>" /></td></tr>
<tr><th>追加拦截路径</th><td><input type="text" name="sba_settings[evil_paths]" value="<?php echo esc_attr($opts['evil_paths'] ?? ''); ?>" style="width:100%" /></td></tr>
</table>
<?php submit_button('保存核心配置'); ?>
</div>
</div>
</form>
<div class="sba-card">
<h3>📡 IP 库同步 (v4: <?php echo number_format($v4); ?> | v6: <?php echo number_format($v6); ?>)</h3>
<form method="post" enctype="multipart/form-data">
<input type="file" name="sba_ip_file" accept=".txt">
<input type="submit" name="sba_import_action" class="button button-primary" value="载入 7 列 IP 数据">
</form>
</div>
</div>
<?php
}
/* ================= 6. 后台逻辑同步 ================= */
add_action('admin_init', function() {
register_setting('sba_settings_group', 'sba_settings');
if(isset($_POST['sba_import_action']) && !empty($_FILES['sba_ip_file']['tmp_name'])) {
global $wpdb; $table = $wpdb->prefix . "sba_ip_data";
$handle = fopen($_FILES['sba_ip_file']['tmp_name'], "r");
if ($handle) {
$first = fgets($handle);
if ($first) {
$p = explode('|', trim($first));
$type = (strpos($p[0] ?? '', ':') !== false) ? 6 : 4;
$wpdb->query($wpdb->prepare("DELETE FROM $table WHERE ip_type = %d", $type));
rewind($handle);
}
$batch = []; $i = 0;
while (($line = fgets($handle)) !== false) {
$p = explode('|', trim($line));
if (count($p) >= 3) {
$s_bin = @inet_pton($p[0]); $e_bin = @inet_pton($p[1]);
if (!$s_bin || !$e_bin) continue;
$batch[] = $wpdb->prepare("(%d, %s, %s, %s)", $type, $s_bin, $e_bin, implode('·', array_slice($p,2)));
if (count($batch) >= 800) { $wpdb->query("INSERT INTO $table (ip_type, start_bin, end_bin, addr) VALUES " . implode(',', $batch)); $batch = []; }
$i++;
}
}
if (!empty($batch)) $wpdb->query("INSERT INTO $table (ip_type, start_bin, end_bin, addr) VALUES " . implode(',', $batch));
fclose($handle);
add_settings_error('sba_settings', 'import', "同步成功:载入 $i 条记录。", 'updated');
}
}
});
然后在wp-admin管理界面插件中启用并配置插件。
四、 使用与常见问题
1. 为什么启用后我被拦截了?
大概率是因为你还没来得及在设置里填入用户名白名单。 解决方法:
- 通过 FTP 临时编辑
site-behavior-auditor.php,在sba_execute_block函数第一行加入return;。 - 刷新后台,进入“防御设置”填好你的用户名并保存。
- 删除代码里的
return;。
2. 数据库性能
插件使用了 ON DUPLICATE KEY UPDATE 和 VARBINARY 索引优化,即使每天有数万次访问,对服务器的负担也极小。
结语: 安全不应该是一个黑盒。通过这套系统,我们可以清晰地看到每一个请求的意图。这不仅是防御工具,更是理解站点运行状态的窗口。


