大致思路:
读取所有网站最近两分钟的日志。
合并统计所有站点的 IP。
超过200次的 IP 写入公共封禁文件。
自动检测并重载 Nginx。
已封禁的 IP 不会重复写入。
这个简化版只处理 IPv4,封禁后不会自动解封,需要手动解除。
第一步:创建公共封禁文件
执行:
touch /www/server/nginx/conf/blockips.conf chmod 644 /www/server/nginx/conf/blockips.conf
编辑 Nginx 主配置:
/www/server/nginx/conf/nginx.conf
在 http {} 中,找到:
include /www/server/panel/vhost/nginx/*.conf;
在它前面添加:
# 所有网站共用的IP封禁名单 include /www/server/nginx/conf/blockips.conf;
最终类似:
http {
# 原来的配置……
include /www/server/nginx/conf/blockips.conf;
include /www/server/panel/vhost/nginx/*.conf;
}检查:
/www/server/nginx/sbin/nginx -t
成功后重载:
/www/server/nginx/sbin/nginx -s reload
作用:以后只要公共文件出现
deny 1.2.3.4;
这个 IP 访问任何网站都会立即收到403,不会进入 PHP。
第二步:在宝塔创建计划任务
进入宝塔:计划任务 → 添加任务
填写:
任务类型:Shell脚本
任务名称:Nginx自动封禁高频IP
执行周期:N分钟
分钟:1
脚本内容:复制下面完整脚本
#!/bin/bash
# 防止上一次任务未结束时重复运行
exec 9>/tmp/nginx-auto-ban.lock
flock -n 9 || exit 0
# ==================================================
# 可以修改的设置
# ==================================================
# 两分钟内访问达到多少次就封禁
THRESHOLD=50
# 如果你有固定管理IP,可以填写
# 没有固定IP就保持为空
ADMIN_IP=""
# 公共封禁文件
BAN_FILE="/www/server/nginx/conf/blockips.conf"
# Nginx程序
NGINX="/www/server/nginx/sbin/nginx"
# ==================================================
# 以下内容通常不需要修改
# ==================================================
# 当前分钟和上一分钟
NOW=$(LC_ALL=C date '+%d/%b/%Y:%H:%M')
PREV=$(LC_ALL=C date -d '1 minute ago' '+%d/%b/%Y:%H:%M')
# 临时文件
OLD_FILE="/tmp/nginx-blockips-old.$$"
NEW_FILE="/tmp/nginx-blockips-new.$$"
IP_FILE="/tmp/nginx-high-frequency-ips.$$"
# 任务结束后删除临时文件
trap 'rm -f "$OLD_FILE" "$NEW_FILE" "$IP_FILE"' EXIT
# 确保公共封禁文件存在
touch "$BAN_FILE"
# 备份修改前的封禁文件
cp -a "$BAN_FILE" "$OLD_FILE"
cp -a "$BAN_FILE" "$NEW_FILE"
# ==================================================
# 读取所有网站最近两分钟的访问日志
# ==================================================
for FILE in /www/wwwlogs/*.log
do
[ -f "$FILE" ] || continue
# 排除错误日志和系统日志
case "$FILE" in
*.error.log|*/nginx_error.log|*/tcp-access.log|*/tcp-error.log)
continue
;;
esac
# 每个网站只读取最后10000行
# 只统计当前分钟和上一分钟
tail -n 5000 "$FILE" 2>/dev/null |
awk -v now="$NOW" -v prev="$PREV" '
{
# 只统计当前分钟和上一分钟
if (index($0, "[" now) == 0 &&
index($0, "[" prev) == 0) {
next
}
# Nginx标准日志中的第7列是请求地址
uri = tolower($7)
# 删除问号后面的查询参数
# 例如:app.js?v=123 变成 app.js
sub(/\?.*/, "", uri)
# 资源文件不参与IP访问频率统计
if (uri ~ /\.(js|css|map|jpg|jpeg|png|gif|webp|bmp|svg|ico|swf|woff|woff2|ttf|eot|otf|mp3|wav|ogg|mp4|webm|avi|mov)$/) {
next
}
# 只输出有效IPv4
if ($1 ~ /^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$/) {
print $1
}
}
'
done |
sort |
uniq -c |
awk -v threshold="$THRESHOLD" '
$1 >= threshold {
print $2
}
' > "$IP_FILE"
CHANGED=0
# ==================================================
# 把达到条件的IP加入公共封禁文件
# ==================================================
while read -r IP
do
[ -n "$IP" ] || continue
# 不封禁服务器本地地址
case "$IP" in
127.*)
continue
;;
esac
# 不封禁管理员固定IP
if [ -n "$ADMIN_IP" ] && [ "$IP" = "$ADMIN_IP" ]
then
continue
fi
RULE="deny $IP;"
# 已经封禁的IP不重复添加
if ! grep -qFx "$RULE" "$NEW_FILE"
then
echo "$RULE" >> "$NEW_FILE"
echo "$(date '+%F %T') 准备封禁IP:$IP"
CHANGED=1
fi
done < "$IP_FILE"
# 没有新IP,不需要修改Nginx
if [ "$CHANGED" -eq 0 ]
then
echo "$(date '+%F %T') 检查完成,没有发现新的高频IP"
exit 0
fi
# 使用新的封禁文件
cp -a "$NEW_FILE" "$BAN_FILE"
# 检测完整Nginx配置
if "$NGINX" -t
then
# 检测成功,平滑重载
"$NGINX" -s reload
echo "$(date '+%F %T') 新IP已经封禁,Nginx重载成功"
else
# 检测失败,恢复修改前的文件
cp -a "$OLD_FILE" "$BAN_FILE"
echo "$(date '+%F %T') Nginx检测失败,已经恢复旧文件"
exit 1
fi