首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >后台登录加固先做什么:权限、双因子、尝试限制,以及改不改登录地址

后台登录加固先做什么:权限、双因子、尝试限制,以及改不改登录地址

原创
作者头像
用户10345225
发布于 2026-11-02 12:37:00
发布于 2026-11-02 12:37:00
990
举报

「验证码装上了,日志里还是一晚上几千次 POST /wp-login.php。」这种问题很常见,毛病多半出在顺序上:把减少噪音的动作放在了防入侵的前面。合理的顺序是先把能登录的账号面收干净,再让「密码正确」不再等于「进得去后台」,然后才是尝试限制、验证码、改路径这些。

验证码为什么挡不住

一次性人机验证加的是单次交互成本。分布式慢速爆破把这个成本摊到大量 IP 上,每个 IP 每分钟只试一两次,永远低于常见的「每 IP 每分钟 10 次」阈值,而针对同一个用户名,一小时就能试掉几百个密码。

实现上的漏洞更致命:验证码校验通过后 token 没有失效,同一个 token 可以重放;校验只挂在登录页的渲染流程里,直接 POST 就绕过去了;只对 action=login 生效,action=lostpassword、action=register、xmlrpc.php 完全不经过它。

验证码更像减速带。先做「就算密码被猜到也进不去」的事。

权限:先把能登录的账号收干净

代码语言:bash
复制
wp user list --role=administrator --fields=ID,user_login,user_email,user_registered
wp user application-password list <user>

要处理的几类:

  • 不需要管理员权限的账号降成 editor / author;离职账号停用,不要几个人共用一个。
  • admin、test、和域名同名的登录名,在爆破字典第一页。改登录名有两条路:新建一个干净账号,把内容迁过去再删旧的;或者直接改 wp_users.user_login,改完要清掉所有会话。注意改「显示名」对登录毫无影响,而且旧登录名还会残留在 user_nicename 和文章作者 slug 里。
  • 应用密码、REST 凭据:没人用的删掉,还在用的记下用途,定期轮换。这些凭据不走登录页,2FA 也管不到它们,权限要压到最小。

双因子:为什么它该排在限速前面

限速改变的是「试多少次」,双因子改变的是「试对了之后能不能进」。后者才是决定损失的那一步。

TOTP、短信、邮件三类里,TOTP 是离线校验,不依赖运营商通道,换卡、漫游、虚拟号都不影响;短信依赖运营商,而且不宜作为唯一因子;邮件等于把后台的安全边界交给了邮箱。以 TOTP 为主,恢复码兜底。

大多数人漏掉的是下面这些运维细节:

  • 恢复码:生成时一次性交付给本人,存密码管理器或纸质。不要在工单系统里留明文。每一枚用过立刻失效。
  • 换机:先在新设备上确认能生成正确的一次性码,再作废旧 secret。顺序反了就是把自己锁在门外。
  • 设备丢失:必须存在第二条路径——另一个管理员能重置,或者服务器上有 wp-cli 能在不登录后台的情况下停用对应插件、删掉该用户的 2FA 数据(键名取决于具体实现,别猜,提前演练一遍并记下命令)。
  • 强制上线:先对 administrator 强制,确认每个人都完成绑定之后再推广到 editor。没确认完就全员强制,会一次性锁死整个团队。
  • 服务账号:跑自动化的账号不能走交互式 2FA,用 application password,并把它的权限和被允许访问的接口限制住。

尝试限制:计数维度比阈值重要

慢速爆破的特征是:单 IP 频率极低,单账号累计次数很高。所以:

  • 主维度是账号。不管从哪个 IP 来,同一个用户名连续失败到阈值就退避。
  • 辅助维度是 IP / 子网。按 /24 或 ASN 聚合,处理单点高频和撞库字典遍历。
  • 全局失败速率只用来告警和临时提高验证难度,不要自动锁全站,否则一个攻击者就能让所有人登不进来。
  • 退避用指数增长,不要固定 15 分钟:第 N 次超额锁 min(60 * 2^over, 3600) 秒。
  • 计数器必须放在多台 web 都读得到的地方。transient 默认落数据库,多节点共享;如果用了每台机器本地的 APCu 或文件缓存,N 台机器的阈值等于被放大了 N 倍。
  • 对外提示统一成「用户名或密码错误」,对内日志分开记「账号不存在」「密码错」「2FA 错」,否则提示本身就成了用户名枚举工具。

下面这个 mu-plugin 是可跑的骨架:

代码语言:php
复制
<?php
/**
 * Plugin Name: Login Throttle
 * Description: 账号维度指数退避 + 全局失败速率观测
 */

const AC_LT_MAX_TRIES  = 5;    // 同一账号窗口内允许的失败次数
const AC_LT_WINDOW     = 900;  // 计数窗口(秒)
const AC_LT_LOCK_BASE  = 60;   // 首次锁定时长(秒)
const AC_LT_GLOBAL_MAX = 60;   // 每 5 分钟全站失败数告警阈值

// 优先级 5:早于核心在 20 上的密码校验,被锁的账号连哈希都不会算一次
add_filter('authenticate', 'ac_lt_check', 5, 3);
function ac_lt_check($user, $username, $password) {
    if (empty($username) || empty($password)) {
        return $user;
    }
    $state = get_transient('ac_lt_' . md5(strtolower($username)));
    if (is_array($state) && !empty($state['until']) && $state['until'] > time()) {
        return new WP_Error(
            'ac_lt_locked',
            sprintf('尝试次数过多,请 %d 秒后再试。', $state['until'] - time())
        );
    }
    return $user;
}

add_action('wp_login_failed', 'ac_lt_record');
function ac_lt_record($username) {
    $now = time();
    $key = 'ac_lt_' . md5(strtolower($username));

    $state = get_transient($key);
    if (!is_array($state) || ($state['first'] + AC_LT_WINDOW) < $now) {
        $state = ['count' => 0, 'first' => $now, 'until' => 0];
    }
    $state['count']++;

    if ($state['count'] >= AC_LT_MAX_TRIES) {
        $over = $state['count'] - AC_LT_MAX_TRIES;
        $state['until'] = $now + min(AC_LT_LOCK_BASE * (2 ** $over), 3600);
    }

    // 过期时间必须覆盖锁定时间,否则锁会被提前清掉
    set_transient($key, $state, AC_LT_WINDOW + 3600);
    ac_lt_bump_global($now);
}

add_action('wp_login', function ($login) {
    delete_transient('ac_lt_' . md5(strtolower($login)));
});

function ac_lt_bump_global($now) {
    $bucket = 'ac_lt_g_' . floor($now / 300);
    $n = (int) get_transient($bucket) + 1;
    set_transient($bucket, $n, 600);
    if ($n > AC_LT_GLOBAL_MAX) {
        error_log(sprintf('[login-throttle] global failures in 5min: %d', $n));
    }
}

两点说明。authenticate 上优先级 20 是核心的密码校验,锁定判断挂在 30 就变成「先算完密码再告诉你被锁了」,既浪费哈希开销,也让日志里分不清是锁定拒绝还是密码错。另外,XML-RPC 和 REST 的密码认证同样走 authenticate 这条链,所以这段代码会一并覆盖,但 2FA 覆盖不到 application password,那部分靠权限收敛。

验证码:一次性消费和前后端分离时的校验

  • 服务端生成 challenge,存在 session 或带签名的 token 里,校验通过立刻删除。同一个 token 第二次提交必须失败。
  • 校验绑时间窗(比如 5 分钟)和来源(session id 或 IP+UA 的哈希),否则 token 能被批量取走再集中使用。
  • 前后端分离时,校验必须发生在服务端登录接口内部,不能只在前端页面做,也别复用页面渲染时生成的那个 challenge。
  • 验证码失败同样计入上面的失败计数,不然攻击者用错验证码无限刷就绕过了限速。
  • 记得覆盖 wp-login.php 的多个入口:action=login、action=register、action=lostpassword、action=resetpass,以及独立于它的 xmlrpc.php 和 REST 认证。

改登录地址:收益和代价得算清楚

收益很具体:自动扫描器打 wp-login.php 的噪音大幅下降,日志干净,WAF 告警变少。

代价也很具体:团队书签、密码管理器里存的 URL 全部失效;监控和合成测试如果探的是 wp-login.php 会开始误报;内部脚本、CI、SSO 回调可能硬编码了登录路径;忘了新地址又没有备用通道,就是自己进不去。

最关键的一点:它不改变任何认证逻辑。xmlrpc.php、REST、application password 都还在,wp-admin/ 未登录时照样跳到登录页。它挡的是扫目录的脚本,挡不住真在撞库的人。

所以判断很简单:目标是减少日志噪音,改;目标是防爆破,先把双因子和账号维度退避做完,再考虑改。真改的话,保留一条备份入口(原路径只放行办公网 IP,或者干脆把整个后台前置到 VPN / SSO 后面),并把新路径写进团队文档和监控配置。

不想自己写这几段的话

上面这些自己动手是一下午的量,最费时间的其实不是代码,是账号盘点、恢复码怎么发、强制 2FA 之后万一有人进不去怎么救这几件流程上的事。

AumCreate 做 WordPress 代部署:模板装到客户自己的主机上,每一页换成客户的内容,交付前不留示例文字,后台这块(管理员账号收敛、应用密码清理、尝试限制、2FA 上线顺序)可以一起做完再交;主机、域名、源码始终是客户自己的,交付后拿 SSH 和 wp-cli 照样能改。$399,材料收齐后 5 个工作日内上线,需要准备什么写在 setup 页。如果你更想自己控制,上面那段 mu-plugin 直接拿去改常量即可,别忘了很多主机面板自带一层登录保护,先看看它做了什么再叠加。

改完怎么验证

  • 从外部机器用错密码连试 6 次,第 6 次要被退避,页面提示要保持统一,日志里留痕。
  • 在锁定状态下用正确密码试一次,必须仍然被拒——这证明锁定判断发生在密码校验之前。
  • 抓一次登录请求,重复提交同一个验证码,第二次必须失败。
  • 删掉本地 TOTP secret 后用恢复码登录,成功一次之后再用同一枚恢复码,必须失败。
  • wp user session list <user> 看会话,wp user session destroy <user> --all 之后旧 cookie 应该立刻失效。

动手之前先确认自己还有 SSH 和 wp-cli。能停插件、能改用户、能清会话,才有资格去动登录这一层;否则第一个被挡在门外的可能是你自己。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 验证码为什么挡不住
  • 权限:先把能登录的账号收干净
  • 双因子:为什么它该排在限速前面
  • 尝试限制:计数维度比阈值重要
  • 验证码:一次性消费和前后端分离时的校验
  • 改登录地址:收益和代价得算清楚
  • 不想自己写这几段的话
  • 改完怎么验证
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档