WordPress后台每天被爆破5000次?用 Nginx + PHP 让 WordPress 登录页“物理隐身”!
开始折腾登录页的起因
今天(2月7号)在 RSS 订阅器里看到宗宗酱的文章《WordPress后台经常被扫》,讲了他感觉自己经常被扫描、被暴力破解,于是更改后台登录地址的事情。我本来看个标题和(RSS阅读器AI写的)摘要,想去评论区“同病相怜”一下的。
结果点进文章链接里一看人家的防火墙截图:本月暴力破解合计 274 次。
我再看看我的防火墙摘要:在我写下这段文字的时间是下午 14 点,当天暴力破解计数已经是 3355 次了,从周统计数据看,本周平均每天都有超过 5000 次的暴力破解。

别人每天被暴力破解十几次就感觉“经常”被暴力破解了,我这都每天几千次了还那么淡定,过分了过分了啊,过于佛系了
虽然因为我的密码长度很长,且登录页允许密码错误尝试次数仅为2次,加上还有 2FA(两步验证)的存在,想要靠暴力破解密码来登录后台几乎是不可能的事情。但这个性能损失实在是太大了。
每一次恶意的登录尝试,WordPress 都要启动 PHP 进程、查询数据库、验证哈希,这都是实打实的服务器资源消耗。这就像虽然家里装了防盗门(2FA)还有个保安(安全插件)只要2次尝试打不开门就直接撵走,但每天有 5000 个人来敲门试锁也不行啊,虽然他们都进不来,但吵也被吵死了。
于是,我决定给登录页做一个“物理隐身”:只有对上了暗号的人,才能看到登录框,否则直接在服务器层面掐断连接。
正文:我是如何一步步把登录页“藏”起来的
核心思路
我要实现的效果很简单:
- 普通的
wp-login.php访问 -> 直接拦截(最好连 404 页面都不给,直接断开连接,省流量)。 - 带暗号的访问
wp-login.php?sky-> 正常显示登录页。 - 我知道有 WPS Hide Login 插件,但就如他自己说的
But there is no redirection via the plugin, the default URL of WordPress(但是没有通过插件重定向 WordPress 的一些默认URL)这是个很多人没注意到的这个插件的缺陷。以及他毕竟是基于 PHP 的还是太重了点。 - 额外提醒:请一定要看完文章,因为文章最开始的代码有巨坑存在,不要直接用,最后会提供完善的代码。
方案试错:网上那些“隐藏登录页”的 PHP 代码为何无效?
直接搜一下“不用插件隐藏WordPress登录页”,满屏都是下边这个示例代码。
add_action('login_enqueue_scripts', 'tb_wp_login_protection');
function tb_wp_login_protection(){
if( !isset($_GET['sky']) ){
header( 'Location: ' . home_url() );
exit;
}
}
我定睛一看,这不就纯纯自欺欺人嘛,这代码仅检测 $_GET['sky'],对 POST 请求完全无效。本质上他只是让登录页/wp-login.php不带参数时,无法在浏览器内打开,问题是你只是隐藏个登录页的打开有毛线用,谁家暴力破解登录是打开登录页手动的尝试登录的。自动化脚本都是直接对 /wp-login.php 提交POST请求的啊!
这段破代码不知道毒害了多少人了,写教程的人真的自己用过这段代码吗?真的懂 WordPress 吗?嗯?Look My Eyes!
第一回合:还是要自己来
查了一下资料,WordPress登录是这样的过程
WordPress 登录流程:
├── 0. login_init
├── 1. login_head
├── 2. login_enqueue_scripts
├── 3. 输出HTML
├── 4. 用户提交表单
└── 5. 验证登录
示例代码是在login_enqueue_scripts阶段插入的,这个时候其实已经加载了一部分资源了,所以应该用 login_init ,在加载登录脚本前就进行验证,这样就可以节约大量的性能。
add_action('login_init', 'tb_wp_login_protection');
function tb_wp_login_protection() {
$secret_key = 'sky'; // 自定义密钥
// 同时验证 GET 和 POST 请求
if( !isset($_GET[$secret_key]) && !isset($_POST[$secret_key]) ) {
header('Location: ' . home_url());
exit;
}
}
部署上去,效果立竿见影。
我用隐身窗口访问 wp-login.php,直接报错。带上参数访问,登录页出来了。
稳!
第二回合:为什么点登录按钮就白屏?
然而打脸来得太快,刚才还在嘲笑别人垃圾代码。现在当我美滋滋地输入账号密码,点击“登录”按钮的一瞬间,页面它白屏了(403)。
原因排查:
我再仔细一看提交时的网络请求。擦,WordPress 的登录表单默认是提交给 POST /wp-login.php 的。点登录按钮后提交的地址里 没!有!带!上!暗!号!!PHP 接到 POST 请求时发现没参数,这不就直接就拦截了吗?。
于是我去拷问了一下 Gemini 怎么改按钮的链接,结果这时候,我发现自己犯了两个错误。
- PHP 烫知识:
$_GET获取的是URL查询字符串参数,并不关心HTTP请求方法是 GET 还是 POST 亦或其他。$_POST获取的是请求体(body)参数,不包含URL参数。所以最开始那个示例代码其实能拦截到直接POST /wp-login.php的。 -
但既然他能拦截到
POST /wp-login.php,但是点击登录按钮提交的 URL 还是 WordPress 默认不带参数的那个,自己把自己拦住了。所以那个示例代码仍然是实际不可用的,垃圾教程污染中文互联网
解决办法:
简单,修改登录表单的 action 地址就行嘛,不过刚才 Gemini 给了我两个建议
- 改成
/wp-login.php?word=sky的形式,由参数名和参数两部分组成,看起来更加规范一点。 - 用
add_query_arg函数构造链接,而不是自己拼接,万一原 URL 已经有参数了呢?
add_action('login_init', function() {
// 如果 URL 里没有 ?word=sky,直接 403
if ( !isset($_GET['word']) || $_GET['word'] !== 'sky' ) {
die('403 Forbidden');
}
});
// 给登录表单的提交地址也加上暗号
add_filter('login_form_action', function($url) {
return add_query_arg('word', 'sky', $url);
});
第三回合:我被系统邮件关在门外
于是我继续开个隐身模式测试,这次登页出来了,登录按钮点击也能生效了,结果因为是隐私模式触发了 WordPress 的陌生设备登录安全机制,发了一封“验证链接”的邮件给我。我点击邮件里的链接,结果……您猜怎么着,诶,它又双叒叕是 403 Forbidden!
原因排查:
WordPress 发出的系统邮件(包括找回密码、重置密码、邮箱验证、退出登录)里的链接,都是官方生成的标准链接,里面就不可能包含我的 word=sky 暗号。
解决办法:
- 虽然可以直接改WordPress的代码,去改链接,这没有可持续性,稍微一个版本更新就无了。
- 就像WPS Hide Login 插件的 Q&A 里说的那样:如果提供了带参数的链接,任何攻击者只要尝试触发链接,就能通过这个链接轻易发现“隐藏”的后台地址。这不是白隐藏了嘛。
既然不能改邮件里的链接,那就得在拦截逻辑里开“白名单”。让合法的系统动作(通过 GET 里的 action 参数判断),比如 lostpassword(找回密码); confirm_admin_email(验证邮箱)之类的动作,即使没有暗号也予以放行。
于是现在隐藏 WordPress 登录页的 PHP 逻辑变成了现在这样
add_action('login_init', function() {
// 1. 如果 URL 里没有 ?word=sky
if ( isset($_GET['word']) && $_GET['word'] === 'sky' ) return;
// 2. 白名单动作
// 这些是Gemini告诉我的 WordPress 系统邮件功能会用到的 action 参数
$allowed_actions = array(
'logout',
'lostpassword',
'retrievepassword',
'resetpass',
'rp',
'confirm_admin_email',
'postpass'
);
// 3. 如果当前请求的 action 在白名单里,也予以放行
if ( isset($_GET['action']) && in_array($_GET['action'], $allowed_actions) ) {
return;
}
// 既没暗号,也不是白名单动作?拦截!
die('403 Forbidden');
});
// 给登录表单的提交地址也加上暗号
add_filter('login_form_action', function($url) {
return add_query_arg('word', 'sky', $url);
});
第四回合:来自 Wordfence 2FA 的“背刺”
本以为这下总该完美了吧?结果我又遇到了“最终 Boss”——WordFence 插件。
因为我开启了 Wordfence 的 2FA(两步验证),输入完密码会跳转到输入 2FA 验证码的界面,只要一点登录,哎,我是丝毫没有意外的被立马拦截了。
原因排查:
被坑了这么多次都已经有经验了, Wordfence 的 2FA 界面实际上是一个独立的流程,它在生成跳转链接和验证表单时,应该是直接调用了 site_url('wp-login.php')。而且,它的验证动作 action=wordfence_2fa_login 也不在我的白名单里。
解决办法:
- 和之前一样,把
wordfence_2fa_login加入白名单。 - 啊啊啊,干脆使用
login_url和site_url过滤器,全局劫持 WordPress 生成的所有登录相关链接,强行把暗号“焊死”在链接上。 - 理论上 全局劫持 应该覆盖所有链接。可惜我试了一下,邮件内的链接貌似劫持不到,而且类似退出之类的动作的重定向 URL 也劫持不到,导致我点了退出会跳到无暗号的登录页去,导致出现 403 白屏,使用体验极差,还是需要额外给特定动作加白。
// A.拦截不带暗号的请求
add_action('login_init', 'protect_login_protection_pro');
function protect_login_protection_pro() {
// 1. 有暗号?放行
if ( isset($_GET['word']) && $_GET['word'] === 'sky' ) {
return;
}
// 2. 定义白名单
$allowed_actions = array(
'logout', 'lostpassword', 'retrievepassword', 'resetpass',
'rp', 'confirm_admin_email', 'postpass', 'wordfence_2fa_login'
);
// 3. 检查白名单与但做 POST 限制
if ( isset($_GET['action']) && in_array($_GET['action'], $allowed_actions) ) {
// 放过WordFence的2FA
if ( $_SERVER['REQUEST_METHOD'] === 'POST' && $_GET['action'] !== 'wordfence_2fa_login' ) {
http_response_code(403);
die('403 Forbidden: POST not allowed for whitelist.');
}
return;
}
// 4. 拦截
http_response_code(403);
die('403 Forbidden.');
}
// 全局劫持链接
// B.全局劫持所有登录链接
add_filter('login_url', 'protect_add_secret_smart', 10, 3);
add_filter('site_url', 'protect_force_secret_smart', 10, 3);
// C.全局劫持表单提交地址
add_filter('login_form_action', 'protect_add_secret_smart');
function jiestyle_add_secret_smart($url) {
return add_query_arg('word', 'sky', $url);
}
function protect_force_secret_smart($url, $path, $scheme) {
if (strpos($path, 'wp-login.php') !== false) {
$url = add_query_arg('word', 'sky', $url);
}
return $url;
}
第五回合:犯傻了,防护了个寂寞
部署新版代码,隐私模式测试,完美,没有任何问题!搞定收工!……了吗?
原因排查:
等我进入后台时想起了一个巨大的问题,全局劫持所有登录链接固然解决了,POST地址不带参数时导致的问题,但刚才第三回合中提到了一个事情,却被我遗忘了:“按照目前的设计,如果攻击者访问白名单动作(比如找回密码),系统生成一个按钮返回。因为有全局注入代码,这个按钮的链接里就会包含暗号!攻击者查看返回的源码就能拿到暗号!”
这就陷入了一个两难境地:
- 注入吧:白名单页面(如找回密码页)上的链接会暴露暗号。(当然防御一般的脚本小子倒是已经够了,自动化脚本如果
curl -I默认登录页,状态不是 200 OK 就跳过站点了) -
不注入吧:自己登录时,表单提交地址里没暗号,点登录就报错。
解决办法:
那就继续打补丁呗,如果当前页面是白名单页面(没带暗号),就不要在生成的链接里加暗号;当前页面已经是正确登录页(带了暗号)时,才继续劫持链接补上暗号。把前边代码 全局劫持链接 部分全删了,替换成下边这个。找回密码之类的时候,麻烦就麻烦点吧。
// 1. 只有当“当前请求”本身就带有暗号时,才激活注入逻辑!
if ( isset($_GET['word']) && $_GET['word'] === 'sky' ) {
add_filter('login_url', 'protect_add_secret_smart', 10, 3);
add_filter('login_form_action', 'protect_add_secret_smart');
add_filter('site_url', 'protect_force_secret_smart', 10, 3);
}
// 2. 特殊情况:如果是 2FA,允许注入
else if ( isset($_GET['action']) && $_GET['action'] === 'wordfence_2fa_login' ) {
add_filter('login_url', 'protect_add_secret_smart', 10, 3);
add_filter('login_form_action', 'protect_add_secret_smart');
add_filter('site_url', 'protect_force_secret_smart', 10, 3);
}
// 注入
function jiestyle_add_secret_smart($url) {
return add_query_arg('word', 'sky', $url);
}
function jiestyle_force_secret_smart($url, $path, $scheme) {
if (strpos($path, 'wp-login.php') !== false) {
$url = add_query_arg('word', 'sky', $url);
}
return $url;
}
终极方案:利用 Nginx 返回 444 实现零资源消耗拦截
虽然 PHP 代码已经完美工作了,但我看着后台日志陷入了沉思。
每天 5000 次爆破,虽然都被 PHP 拦截了,但每一次请求,Nginx 都要把请求转发给 PHP-FPM,PHP 都要启动进程、加载环境、执行代码、输出 403。这依然是在消耗服务器资源嘛,虽然 WordPress 核心并没有完整启动,性能消耗并不算很大
但如果我能在 Nginx 层面就直接拦截,让这些请求连 PHP 的面都见不着,岂不是更省资源?
给你们看一组实测的直观对比:
- PHP 方案 (PHP-FPM):在 WordPress 环境下,启动一个 PHP-FPM 进程(为了处理我的 login_init 拦截逻辑)起步就需要 50MB 的内存。即便只是输出一个 403 Forbidden,也要完整经历“分配进程 -> 加载 PHP 环境 -> 执行拦截脚本”这一套流程,在高并发爆破下,CPU 占用分分钟能冲上 50% 。
-
Nginx 方案:Nginx 是基于异步事件驱动的,处理这样一个带参数判断的请求,内存开销不到 2MB,而 CPU 占用几乎是 0.1% 甚至更低。
于是,我把上面的 PHP 逻辑“翻译”成了 Nginx 配置,利用 Nginx 的 return 444(该状态码会直接关闭 TCP 连接,不返回任何数据),让攻击者的扫描器直接超时或连接重置,真正实现登录页的“物理隐身”。
高效的 Nginx 防御配置,专门针对 WordPress wp-login.php 进行保护(放在 server 块中):
# ⚡️ Nginx 登录页隐身术
# 默认开启拦截 (1)
set $block_login 0;
# 1. 只有访问 wp-login.php 时才开启检测
if ($uri ~* "^/wp-login\.php") {
set $block_login 1;
}
# 2. 【规则一】如果携带了正确的暗号 word=sky,放行
if ($arg_word = "sky") {
set $block_login 0;
}
# 3. 【规则二】白名单动作 (对应 PHP 里的白名单)
# 必须包含 wordfence_2fa_login 否则 2FA 会挂
if ($arg_action ~* "(logout|lostpassword|retrievepassword|resetpass|rp|confirm_admin_email|postpass|wordfence_2fa_login)") {
set $block_login 0;
}
# 4. 执行拦截:直接返回 444 (关闭连接,连 HTTP 头都不给)
if ($block_login = 1) {
return 444;
}
总结
现在的架构是:
- 让 Nginx 充当“门神”:负责处理那 99.9% 的恶意扫描,直接掐断连接,零资源消耗。
- 让 PHP 充当“补盲”:负责给合法的登录请求自动补全暗号,确保我自己能正常进来。
- 这代码写的如此复杂,浪费了我3、4天的摸鱼时间,为了补上各种各样的漏洞和问题,打了一个又一个的补丁,早知道 PHP 防御部分直接用WPS Hide Login 插件了。😂
- 不过虽然插件方便,但 Nginx 方案性能更好,对于这种暴力爆破拦截的越靠前越好。比如,如果你用 Cloudflare 的话,直接 WAF 上一条规则就行(把前边 negix 逻辑扔给 AI,让 AI 帮你实现就行,我就不写了)

这就是我如何把每天 5000+ 次的报警日志变成 0 的全过程。
PS:最近我换了博客的出口机(最近新加坡到国内非优化线路三网全都绕美),从新加坡普通线路换到香港三网优化线路,大家访问本站的速度有没有感觉变快一点。


慢读时光
2026-02-11 16:43
看了下nginx日志,我的也有很多乱七八糟的请求,只要我不看,就当它不存在。只是很多垃圾评论比较烦人,每天都有很多。
去年夏天
2026-02-11 16:50
哎,日志里奇怪的东西太多了。扫描器的,非善意爬虫的,PCDN刷流的、爆破密码的、试图恶意利用外链跳转页的……没完没了。
学游渊
2026-02-11 16:24
手搓一个啊,这么强。涨姿势了
去年夏天
2026-02-11 16:46
其实本来我是感觉这东西,应该几行代码就搞定了,所以不打算用插件实现。结果写到后面越写越复杂,需要考虑的事情越多,但是前边都研究那么久了,放弃的话,沉默成本太高。早知道这么麻烦确实不如直接用插件了。😂
J.sky
2026-02-11 15:00
记得很久很久以前折腾过WordPress,部署完先改后台登录和登录请求的地址,然后再加装其他的防御插件,我甚至都会删除登录使用的文件,每次自己要登录的时候再上传,那时候还用的FTP登录上传和删除的。
我现在都是静态资源了,每天还会被当做WordPress来扫描的。
去年夏天
2026-02-11 15:27
扫描器主打一个广撒网,先试探有什么,再看能不能用。
删除登录使用的文件:我当初就是这样干的,直接把WP-admin改名,每次需要时再改回来。后来感觉太折腾了,而且当时手机上的FTP/SSH还不好用,每次手机上想改下文章还需要用电脑去操作。于是听了群里大佬的建议:你把门拆了藏起来,不如把门加固就好,反正他们也进不来。
Hary
2026-02-11 13:56
WP还是受欢迎啊,不管什么类型的网站都被扫,相比较TY就很少遇到这种情况
去年夏天
2026-02-11 14:49
天天来,天天扫,最近开了台新服务器,从我收到初始化邮件到我进系统这2分钟里,SSH就有2000+次爆破记录了……
Hary
2026-02-11 14:50
这种估计是这个ip不干净
去年夏天
2026-02-11 15:17
肯定是,估计IP刚被收回来就重复利用给我了
旺东
2026-02-11 13:37
我博客是emlog,EdgeOne帮我承受了一切来自wp的漏洞攻击!🥲
去年夏天
2026-02-11 13:56
没备案用不了 Edgeone ,用了就变减速器了。这帮脚本也怪,我这明明不可能是 Windows 架构的,日志里还是一堆尝试 asp 漏洞点位的。
obaby
2026-02-11 13:16
这个不错
国内的教程,你懂的,多数都是复制粘贴
去年夏天
2026-02-11 13:58
一帮到处搬运刷量的营销号😓