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:最近我换了博客的出口机(最近新加坡到国内非优化线路三网全都绕美),从新加坡普通线路换到香港三网优化线路,大家访问本站的速度有没有感觉变快一点。


老T
2026-03-04 19:41
这种攻击都是基操,每台在线的vps每天都要经历无数次,次数多寡与地区、ip段、服务商有关。不只是扫wordpress路径,更常扫的是ssh和mysql端口。单就wordpress来说,登录页面其实算比较安全的,只要不是简单密码,基本也没人会闲到去搞定向爆破。反倒是wp路径下方一些异常插件、主题文件才是风险更大的。另外还有数据库。
去年夏天
2026-03-05 09:21
是的是的,个人站点登录页其实只要不是存在弱密或密码泄露,基本不太可能被爆破登录,其他地方风险点更高。
主要是看着烦以及影响瞬时性能。
毕竟只要他试图登录,PHP就需要起一个线程去处理。不如尽早在WAF或Nginx就拦截掉。
威言威语
2026-03-01 13:58
php防御终究没有nginx来的方便。
去年夏天
2026-03-01 15:19
是的是的,PHP能写的判断逻辑更复杂但不如Nginx高效
鸟叔
2026-02-22 13:58
文章太专业,看的一头雾水
wu先生
2026-02-18 10:59
历害,让我一个不懂代码的人都看得津津有味。
晓空
2026-02-17 20:09
我之前高中的时候也被这个问题困扰过,而且爆破的比你这边说的还要严重的多,当时我完全就是靠着仅允许尝试一次+错误第一次锁定全站24小时+第二次同ip错误全站锁定7天,这样的
毕竟高中那会儿也没多少时间能上后台来,而且只要我浏览器没过期cookies也不用登录,真的实在不行后面去数据库给自己放行也成,考虑到这里,其实当时甚至还考虑要不干脆把wp-login给删了算了,用到的時候再放回来(主打的就是一个伤敌一千自损八百)
其实光拦截wp-login还不太够,也有很多爬虫喜欢扫描xmlrpc,这边更是重灾区
我目前的话是全部靠OIDC外置登录了,变相的也算是把火力吸引到sso平台上,减轻wp这边的压力,同时也算是减少了wp的攻击面吧,总之目前感觉还行
就是sso平台自托管也是个头疼的问题,主流的方案基本上都出过大洞(半斤八两),还是得勤更新
去年夏天
2026-02-18 22:44
我一度考虑过直接做个蜜罐,直接访问 wp-login 是个静态页面,按钮点击后永远只会直接返回密码错误。
一叶竹
2026-02-14 19:26
我禁止国外ip访问后,这种爆破的少了很多,ssh端口我都直接禁用了。
去年夏天
2026-02-16 17:34
之前尝试过直接在WAF层面阻止一些路径境外访问,结果我自己时不时也会肉身在境外,我也没搞回国节点,每次都要手动去改配置也挺麻烦的,就放弃这个方案了
蒙需
2026-02-13 21:22
我有个文学站也是这样,后台流量特别大。我采用限制除允许的 IP 外可登入的方案处理的,后台访问地址未变。
去年夏天
2026-02-13 21:56
能白名单 IP 登录方案的话就最好了,安全性大幅提升。
Vind
2026-02-13 16:30
刚才打错了,我明明不是 WordPress,被访问的页面也都是 wp-xxx
去年夏天
2026-02-13 22:07
扫描器主打一个广撒网试试看。
Vind
2026-02-13 16:28
我明明是WordPress,但是每天的访问记录里也有在访问 wp-login,wp-admin这种。
林林
2026-02-12 20:55
都安装Wordfence了,不如就用它的登陆强力保护功能吧。
去年夏天
2026-02-12 21:20
就是它每天拦截了5000多次😮💨
每次被爆破时就会引起一次 CPU 波动,现在用 nginx 在最前边拦截几乎没什么波动了。
acevs
2026-02-11 20:17
我装了个杀毒还是啥插件感觉没遇到这个。
去年夏天
2026-02-11 20:31
一阵一阵的,一般爆破个十天半个月的就自己走了。这次攻击全是荷兰的主机,前一段全是香港 ucloud 的主机。
宗宗酱
2026-02-11 19:20
哈哈哈,我这儿是小巫见大巫啊,学习到了!
去年夏天
2026-02-11 20:29
要不是你的文章,我估计一时半会儿没动力去搞🤭
maqingxi
2026-02-11 19:18
你这个代码高亮插件,色彩看着很舒服。
去年夏天
2026-02-11 19:47
语法高亮的主题是 Tomorrow Night ,你可以找找,应该是个通用主题
陈祈星
2026-02-11 18:52
是的 这个最近特别常见 我的两个站都开始被扫了 问题是有一个站刚注册域名
去年夏天
2026-02-11 19:32
黑产确实很是勤奋
慢读时光
2026-02-11 16:43
看了下nginx日志,我的也有很多乱七八糟的请求,只要我不看,就当它不存在。只是很多垃圾评论比较烦人,每天都有很多。
去年夏天
2026-02-11 16:50
哎,日志里奇怪的东西太多了。扫描器的,非善意爬虫的,PCDN刷流的、爆破密码的、试图恶意利用外链跳转页的……没完没了。
学游渊
2026-02-11 16:24
手搓一个啊,这么强。涨姿势了
去年夏天
2026-02-11 16:46
其实本来我是感觉这东西,应该几行代码就搞定了,所以不打算用插件实现。结果写到后面越写越复杂,需要考虑的事情越多,但是前边都研究那么久了,放弃的话,沉默成本太高。早知道这么麻烦确实不如直接用插件了。😂
学游渊
2026-02-11 17:04
最后还是成了嘛,不亏≡ω≡
去年夏天
2026-02-11 17:23
幸好这次最后成了,之前折腾主题,折腾了1周没搞定,快照回档了。
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
一帮到处搬运刷量的营销号😓