本文约 5,339 字,预计阅读 14 分钟。文中命令与配置均经过实测,可直接参考执行。
引言:后台每天被扫几千次
看一下 Nginx 访问日志就知道了:
grep -c "wp-login.php" /var/log/nginx/access.log
# 结果可能是几万
自动化扫描器 24 小时不停尝试 admin / administrator / 常见密码字典。即使你有强密码 + 双因素,这些请求也在消耗服务器资源,且有 0day 漏洞时被撞上的风险。
最彻底的方案:只放行你自己的 IP 访问后台。扫都扫不到,攻击面直接归零。
原理分析:要保护哪些路径
WordPress 后台相关的入口:
| 路径 | 作用 | 是否可限制 |
|---|---|---|
/wp-admin/ |
后台全部页面 | 可限制(但见下文注意事项) |
/wp-login.php |
登录页 | 可限制 |
/wp-admin/admin-ajax.php |
前后台 AJAX 接口 | 不能全限制,前台功能会挂 |
/wp-admin/admin-post.php |
表单提交 | 不能全限制 |
/wp-json/wp/v2/* |
REST API | 部分接口前台在用,谨慎 |
/xmlrpc.php |
远程发布 | 建议直接封 |
关键陷阱:很多人一刀切把 /wp-admin/ 全封了,结果发现:
- 前台的 AJAX 功能(如”加载更多”、联系表单、评论提交)全部失效;
- 因为
admin-ajax.php就在/wp-admin/目录下。
正确做法:放行 admin-ajax.php 和 admin-post.php,只保护其他后台页面;或者更精细地判断 Referer。
保姆级实操
方案一:Nginx 层限制(推荐,性能最好)
server {
# ... 其他配置
# 1) 保护登录页
location = /wp-login.php {
allow 1.2.3.4; # 你的固定 IP
allow 203.0.113.0/24; # 你的公司网段
deny all;
include fastcgi_params;
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
# 2) 保护后台,但放行 AJAX 和必要的公开文件
location ^~ /wp-admin/ {
# 先放行前台功能依赖的接口
location ~ ^/wp-admin/(admin-ajax\.php|admin-post\.php)$ {
include fastcgi_params;
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
# 这里不做 IP 限制
}
# 放行后台的静态资源(CSS/JS,登录页要用)
location ~* ^/wp-admin/.*\.(css|js|png|jpg|svg|woff2?)$ {
expires 7d;
}
allow 1.2.3.4;
allow 203.0.113.0/24;
deny all;
location ~ \.php$ {
include fastcgi_params;
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
}
# 3) 直接封 xmlrpc.php
location = /xmlrpc.php {
deny all;
access_log off;
log_not_found off;
}
}
注意 allow/deny 的顺序:Nginx 是从上往下匹配,命中即停止,所以 deny all 必须放最后。
方案二:Apache(.htaccess)
# 保护 wp-login.php
<Files wp-login.php>
Require ip 1.2.3.4
Require ip 203.0.113.0/24
Require all denied
</Files>
# 保护 wp-admin
<Directory "/var/www/mimangfei/wp-admin">
<FilesMatch "^(admin-ajax\.php|admin-post\.php)$">
Require all granted
</FilesMatch>
Require ip 1.2.3.4
Require all denied
</Directory>
Apache 2.4 的 Require 指令默认取第一个成功匹配,Require all granted 要写在前面。如果逻辑混乱,用 <RequireAny> 包起来:
<Directory "/var/www/mimangfei/wp-admin">
<RequireAny>
<FilesMatch "^(admin-ajax\.php|admin-post\.php)$">
Require all granted
</FilesMatch>
Require ip 1.2.3.4
</RequireAny>
</Directory>
方案三:加一层 HTTP Basic 认证(动态 IP 也适用)
如果 IP 不固定(家里宽带、出差),IP 白名单很痛苦。改用 Basic Auth——先过账户密码,再看到 WordPress 登录页,双保险。
# 生成密码文件
sudo apt install apache2-utils -y
sudo htpasswd -c /etc/nginx/.wpadmin_htpasswd mimangfei
location = /wp-login.php {
auth_basic "Restricted";
auth_basic_user_file /etc/nginx/.wpadmin_htpasswd;
include fastcgi_params;
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
location ^~ /wp-admin/ {
auth_basic "Restricted";
auth_basic_user_file /etc/nginx/.wpadmin_htpasswd;
# ... PHP 处理
}
注意:Basic Auth 和 fastcgi_cache 同时用时,被保护的路径要关缓存(前面已处理)。
方案四:插件层(没有服务器权限时)
| 插件 | 功能 |
|---|---|
| Wordfence | 有”只允许特定 IP 登录”、双因素、限流 |
| iThemes Security(Solid Security) | 后台隐藏 + IP 白名单 |
| WPS Hide Login | 把 /wp-login.php 改成自定义路径 |
| Cerber Security | 强大的访问控制和白名单 |
插件方案的优点是不用改服务器配置,缺点是请求已经进了 PHP,防护层级靠后,且依赖插件本身不出漏洞。
方案五:改后台路径(WPS Hide Login)
把登录地址从 /wp-login.php 改成 /my-secret-login,扫描器直接 404。这是成本最低、最实用的措施之一,但属于”安全通过模糊”(security through obscurity),不能替代其他措施。
动态 IP 的应对办法
- DDNS + 脚本更新白名单:家里宽带装 DDNS,服务器上跑定时任务更新 Nginx 配置里的 IP;
bash
#!/bin/bash
IP=$(dig +short myhome.ddns.example)
OLD=$(grep -oP 'allow \K[0-9.]+(?=;)' /etc/nginx/conf.d/wpadmin.conf | head -1)
if [ "$IP" != "$OLD" ]; then
sed -i "s/allow $OLD;/allow $IP;/" /etc/nginx/conf.d/wpadmin.conf
nginx -t && systemctl reload nginx
fi
- 用 VPN / 跳板机:固定一台云主机做跳板,白名单只放行跳板机 IP;
- Basic Auth(见方案三):最省事;
- Cloudflare Zero Trust / Access:免费版可配策略,按邮箱验证后才放行后台路径。这是体验最好的方案。
常见踩坑
- 把自己锁在门外:改配置前先确认自己的公网 IP(
curl ifconfig.me),改完不要关当前浏览器标签,用另一个浏览器或无痕窗口测试。 - 前台 AJAX 挂了:
admin-ajax.php被封,表现为”加载更多”无反应、联系表单提交失败、评论提交转圈。必须放行。 admin-post.php被封:前台的非 AJAX 表单(如某些订阅、下载)会 403。- 案例:某站封了整个
/wp-admin/,当天就收到用户反馈”商品加入购物车失效”。WooCommerce 的加购走admin-ajax.php,被一起封了。改为只封/wp-admin/下的其他 PHP 文件后恢复。 - CDN / 反向代理下拿到的是 CDN IP:
allow匹配的是$remote_addr,如果前置了 Cloudflare,这里是 CDN 节点 IP。必须先用 反向代理下获取真实 IP 的方法还原,或者在 Cloudflare 层用 WAF 规则做限制。 - 忘了保护
/wp-login.php?action=register:只封了/wp-admin/没封/wp-login.php,登录页还是暴露的。两个都要封。 - Basic Auth 与 WordPress 登录冲突:两者是独立的,都要输。忘了 Basic Auth 密码会以为 WordPress 密码错了。
- 日志看不到真实攻击了:封 IP 后日志里全是 403,注意别因为日志”干净了”就以为没人攻击。定期看被拒次数:
bash
grep "wp-login.php" /var/log/nginx/access.log | awk '{print $1}' | sort | uniq -c | sort -rn | head
总结
后台 IP 白名单是投入产出比最高的 WordPress 安全措施。落地要点:Nginx 层做(性能最好、拦得最早)、必须放行 admin-ajax.php 和 admin-post.php、优先改用 Basic Auth 或 Cloudflare Access 应对动态 IP、改前先确认自己的 IP 并保留一个测试窗口。配合强密码、双因素认证、双因素插件,后台基本可以高枕无忧。更全面的安全加固清单可以看WordPress 月度安全巡检清单。
