☰
MySQL连接控制插件实战:防暴力破解的延迟机制与参数调优
2026/9/28 6:36:23 网站建设 项目流程

我接手过不少 MySQL 实例,第一件事永远不是看性能,而是先翻错误日志里 Access denied 出现的频率。有一次半夜两点,日志里每分钟刷几十条Access denied for user 'root'@'...' using password: YES,3306 端口又被人拿字典撞库了。这类攻击不算高明,但特别烦人:IP 可以换、用户名可以换,你封一个它换一个。MySQL 其实自带一个专门治这个问题的插件,叫 Connection Control(连接控制插件),干的事情很纯粹:当某个客户端连续输错密码达到阈值后,服务器开始对它的连接请求做递增延迟,让暴力破解在时间上耗不起。这篇文章就从安装、调参到验证效果完整过一遍,适合手里有 MySQL 5.7 或 8.0 实例、想让数据库自己扛住一部分撞库压力的同学,也适合刚接触数据库安全、想给团队加一道保险的新手。

1. 为什么数据库层面需要防暴力破解

1.1 攻击者是怎么打你的 3306 端口的

暴力破解的核心逻辑其实很土:拿一份弱口令字典,然后用工具批量尝试用户名和密码组合。MySQL 的安全问题里,弱口令被爆破的占比一直不低。很多团队觉得“我有云安全组”“我在内网”“端口没对外开放”,但实际上总有漏网的时候:某个测试环境把 3306 映射出去了、某个临时加白名单忘了关、或者是内网里一台机器被攻破后,攻击者横向移动时扫内网网段找 3306 继续撞库。数据库层面的弱口令爆破和网站后台爆破一样,是内网渗透标准流程里必不可少的一步。

一旦账号被撞出来,攻击者拿到的不是一台服务器,而是你整个数据集的读取和删除权限。轻则数据被拖走,重则直接被删库勒索。所以防爆破不该被当成安全团队的事,DBA 自己就该把它当成基本功。而真正落到生产环境,你需要的不是“知道有人在打你”,而是 MySQL 自己能先撑住一阵子,把攻击节奏拖慢,给你留出封 IP、改密码的时间。

1.2 MySQL 自带的安全网为什么不够用

MySQL 默认有两个东西和防爆破沾边,但都不够。第一个是错误日志里反复出现的Access denied for user,它只负责记录,不负责拦截。第二个是max_connect_errors参数,默认 100,当某个客户端“不能正常完成握手”的次数超过这个值,服务器会直接拒绝该主机的后续连接。问题在于,它统计的是 TCP 握手和连接建立阶段就出问题的连接,而密码错误其实已经走完了握手、进入了认证阶段,根本不怎么计入这个计数里。换句话说,攻击者只要老老实实建立连接、提交错误密码,max_connect_errors大概率一直不触发。

MySQL 8.0.19 之后,CREATE USER和ALTER USER支持了FAILED_LOGIN_ATTEMPTS和PASSWORD_LOCK_TIME,可以让账号连续失败 N 次后直接锁定。这个机制很有用,但它是“硬锁”,存在一个双刃剑问题:攻击者如果故意对一个高权限账号乱输密码,可以把正常账号也锁掉,等于变相制造拒绝服务。所以生产环境里,纯靠“锁死”并不能解决所有场景。

1.3 Connection Control 的设计思路

Connection Control 的思路是“延迟而不是锁死”。它不会让账号失效,而是让服务器对失败连接请求的响应越来越慢。对正常的同事来说,偶尔手滑输错一两次密码,几乎无感知;但对一个每秒尝试几十次密码的爆破工具来说,延迟是致命的,每一轮尝试都要多等好几秒,整个字典跑完的时间成本会从几分钟变成几天。

它还有一个很有价值的特性:计数是分账号维度的,具体是user@host维度。也就是说,某个账号连续失败才会被延迟,不同账号之间的计数不串扰。再加上它支持最大延迟上限,可以避免延迟无限拉长导致自己人都连不上。当然,它也有边界:对于从海量 IP 轮流来试同一个账号的分布式攻击,单账号计数会被分散,插件容易误以为“没有攻击”。所以它是纵深防御里的第一道防线,不是唯一防线,这一点后面会展开说。

2. 安装与启用:两分钟让插件跑起来

2.1 先搞清楚两个插件的关系

Connection Control 实际是两个插件,名字很容易混淆:CONNECTION_CONTROL和CONNECTION_CONTROL_ADMIN。前者负责真正干活的部分——监听连续失败次数、计算并执行延迟;后者负责“管理面”——提供connection_control_*系统变量和INFORMATION_SCHEMA.CONNECTION_CONTROL_FAILED_LOGIN_ATTEMPTS视图。如果只装CONNECTION_CONTROL,你连SHOW VARIABLES LIKE 'connection_control%'都查不到参数,所以官方文档里的标准操作也是两个一起装。

2.2 在线安装与验证

安装命令很简单,在 MySQL 客户端里直接执行:

INSTALL PLUGIN CONNECTION_CONTROL SONAME 'connection_control.so'; INSTALL PLUGIN CONNECTION_CONTROL_ADMIN SONAME 'connection_control.so';

装完验证一下:

SHOW PLUGINS; -- 或者更精确地查 SELECT PLUGIN_NAME, PLUGIN_STATUS FROM INFORMATION_SCHEMA.PLUGINS WHERE PLUGIN_NAME LIKE 'connection%';

两个插件状态都是 ACTIVE 就说明加载成功。需要说明的是,connection_control.so这个文件是 MySQL 安装包自带的,官方二进制 tar.gz、RPM 包、Docker 官方镜像里都有。我分别在 5.7.44 和 8.0.36 的社区版上试过,都能正常加载。如果INSTALL PLUGIN报ERROR 1126 (HY000): Can't open shared library 'connection_control.so',先执行SHOW VARIABLES LIKE 'plugin_dir';查看插件目录,再去系统里 ls 一下这个文件在不在,不在就说明装的是精简版或者路径不对,重新装完整的 mysql-server 包即可。

2.3 让配置重启后依然生效

INSTALL PLUGIN本身会把插件注册到mysql.plugin系统表,重启后会自动加载。所以在最简场景下,你不需要额外写配置。但生产环境一般还会把参数直接写进配置文件,方便统一管理和自动化部署:

[mysqld] plugin-load-add=connection_control.so loose-connection-control-failed-connections-threshold=5 loose-connection-control-min-connection-delay=1000 loose-connection-control-max-connection-delay=60000 loose-connection-control-failed-connections-limit=0

这里有两个细节容易踩坑。第一,loose-前缀很关键,它的意思是“这个变量如果存在就加载,不存在也别报错”。万一哪天插件没加载成功,MySQL 不会因为找不到一个不认识的变量而拒绝启动。第二,plugin-load-add和plugin-load的区别在于前者是追加加载,后者如果多处配置容易互相覆盖。如果你已经在配置里写plugin-load-add,就不需要再用INSTALL PLUGIN,二选一即可;如果之前已经INSTALL PLUGIN过了,再写plugin-load-add也不会怎么样,但没必要重复。

运行时可以直接用SET GLOBAL改参数,立即生效,不用重启:

SET GLOBAL connection_control_failed_connections_threshold = 5; SET GLOBAL connection_control_min_connection_delay = 1000;

但SET GLOBAL改的东西重启后会丢,想持久化还是要写配置文件。用 Docker 跑 MySQL 的话,把配置片段放到挂载进容器的/etc/mysql/conf.d/目录下,重启容器即可。

2.4 卸载也别搞错顺序

卸载时顺序和安装相反,先卸管理员插件,再卸核心插件:

UNINSTALL PLUGIN CONNECTION_CONTROL_ADMIN; UNINSTALL PLUGIN CONNECTION_CONTROL;

同时把my.cnf里对应的plugin-load-add和loose-开头的参数删掉,不然重启后又会自动装回来。这个顺序我踩过坑,反过来先卸核心插件,管理员插件会残留或者报依赖错误,老老实实按顺序走一遍最省心。

3. 核心参数与延迟计算原理

3.1 四个系统变量对照表

Connection Control 一共四个系统变量,版本不同数量不同。5.7 里只有前三个,8.0.13 之后多了第四个,整理成表格看得更清楚:

系统变量默认值作用
connection_control_failed_connections_threshold3连续失败多少次后开始延迟;设为 0 表示关闭延迟功能
connection_control_min_connection_delay1000最小延迟毫秒数,同时也是每次失败的递增步长
connection_control_max_connection_delay2147483647最大延迟毫秒数,默认值约等于 24.8 天,相当于不封顶,生产建议调小
connection_control_failed_connections_limit08.0.13+ 新增,连续失败达到该值后封禁账号;0 表示不封禁

默认阈值是 3。这个默认值的意思是“连续错三次就开始算账”。正常人确实很少连着输错三次密码,但应用连接池如果配置错误,可能一秒内就连续失败很多次,所以很多生产环境会把阈值调到 5 甚至更高,避免误伤。min_connection_delay默认 1000 毫秒,也就是从 1 秒起步。

3.2 延迟计算公式:为什么越错越慢

延迟不是固定的,而是线性递增的,公式可以写成这样:

D = 0 当 N <= T D = MIN((N - T) * S, M) 当 N > T N = 该账号连续失败次数 T = connection_control_failed_connections_threshold S = connection_control_min_connection_delay M = connection_control_max_connection_delay

拿 T=3、S=1000ms、M=60000ms 举例,实际的延迟表现是这样的:

连续失败次数是否延迟本次失败要被拖多久
第 1~3 次否0
第 4 次是1 秒
第 5 次是2 秒
第 6 次是3 秒
第 7 次是4 秒
...是继续递增
第 63 次及以上是封顶 60 秒

这个线性增长的效果非常可观。如果 T=5、S=1000ms、M=60000ms,理论上一个攻击者对同一账号跑 10 万次字典,前 5 次没有延迟,第 6 次开始每次至少等 1 秒,到第 65 次以后每次都要等满 60 秒,光时间成本就从几分钟变成几十天,绝大多数批量爆破工具根本跑不完。

但我想强调一点:这条防线防的是“少数账号被高频尝试”的情况。如果攻击者同时用几万个账号各试一次密码,每个账号的失败次数都到不了阈值,插件就相当于没启动。这种情况要靠网络层防护去兜底,后面第 5 节会说怎么配合。

3.3 计数在哪里看、什么时候清零

插件的失败计数可以在系统视图里查到:

SELECT * FROM INFORMATION_SCHEMA.CONNECTION_CONTROL_FAILED_LOGIN_ATTEMPTS\G

这个视图只展示失败计数大于 0 的账号,字段就两个:USERHOST和FAILED_ATTEMPTS。注意USERHOST是一个带引号的完整字符串,显示格式像'root'@'localhost',查询时直接SELECT *就行,不要想着拆字段去比对,容易出错。

计数清零的时机有两个:该账号成功登录一次,或者整个 MySQL 实例重启。这个特性很重要,意味着如果你的测试账号把自己刷进了延迟,用正确密码登录一次就能恢复正常。

另外还有一个状态变量值得盯:

SHOW GLOBAL STATUS LIKE 'Connection_control_delay_generated';

这个变量统计服务器一共生成过多少次延迟,它是一个只增不减的计数器。如果它突然快速增长,基本可以断定当前有程序在批量尝试失败登录,这时候就该去查错误日志和网络来源了。

4. 实战:模拟一次撞库,眼见为实

4.1 准备一个测试账号

为了让效果可复现,我建议在本地起一个测试账号,不要直接在公网实例上玩。执行下面几条 SQL:

CREATE USER 'brutetest'@'127.0.0.1' IDENTIFIED BY 'Pass_111';

这个账号只需要能走通认证就行,不需要任何业务权限。注意127.0.0.1和localhost在 MySQL 里是两个不同的 host,后面测试脚本里要固定用127.0.0.1去连,不然计数会对不上。

4.2 用脚本连续输错密码

先设一个比较敏感的配置方便观察:

SET GLOBAL connection_control_failed_connections_threshold = 3; SET GLOBAL connection_control_min_connection_delay = 1000; SET GLOBAL connection_control_max_connection_delay = 60000; SET GLOBAL connection_control_failed_connections_limit = 0;

然后用 shell 循环连续输错密码,并且记录每次的耗时:

for i in $(seq 1 8); do s=$(date +%s%N) mysql -h127.0.0.1 -ubrutetest -pWrongPass --connect-timeout=3 -e "SELECT 1" >/dev/null 2>&1 e=$(date +%s%N) echo "第 ${i} 次失败,耗时 $(( (e - s) / 1000000 )) ms" done

如果机器上装了 pymysql,也可以用 Python 脚本,输出更直观:

import time import pymysql for i in range(1, 9): t0 = time.time() try: pymysql.connect( host="127.0.0.1", port=3306, user="brutetest", password="WrongPass", connect_timeout=3, ) except pymysql.err.OperationalError as e: print(f"第 {i} 次:失败 用时 {time.time() - t0:.2f}s {e}") else: print(f"第 {i} 次:竟然成功了") time.sleep(0.3)

正常情况下你会看到前三次失败基本秒回,第四次开始出现 1 秒左右的耗时,第五次 2 秒,第六次 3 秒。这就是插件在生效:每一次失败的响应都在变慢,攻击者拿不到结果就不知道该继续还是换密码。

4.3 从失败计数表和状态变量验证

跑完脚本后查一下计数:

SELECT * FROM INFORMATION_SCHEMA.CONNECTION_CONTROL_FAILED_LOGIN_ATTEMPTS\G

输出大概是:

*************************** 1. row *************************** USERHOST: 'brutetest'@'127.0.0.1' FAILED_ATTEMPTS: 8

这里记录的 8 就是刚才连续失败的次数。此时用一个正确密码登录一次:

mysql -h127.0.0.1 -ubrutetest -pPass_111 -e "SELECT 1"

再重新查这张表,能看到这一行消失了,因为成功登录清零了计数。这个特性在实际运维中很有用:测试环境误伤了自己人,成功登录一次就能解除延迟状态,不需要重启数据库。

4.4 更进一步:8.0 的封禁模式和账号锁双保险

如果你的实例是 8.0.13 以上,可以开启封禁模式,让失败次数达到上限后直接拒绝对应账号的后续连接:

SET GLOBAL connection_control_failed_connections_limit = 8;

开启后继续用错误密码测试,你会发现在连续失败 8 次之后,第 9 次连接不再走“延迟然后拒绝”的流程,而是在当前延迟窗口内直接被拒绝。测试完记得把参数调回 0,避免封住了自己的运维账号。

8.0.19 以上,还可以用账号级别的硬锁策略:

ALTER USER 'brutetest'@'127.0.0.1' FAILED_LOGIN_ATTEMPTS 3 PASSWORD_LOCK_TIME 2;

这个配置的意思是:该账号连续失败 3 次后锁定 2 天,期间就算密码输对了也进不来。如果测试时把自己锁了,用ALTER USER 'brutetest'@'127.0.0.1' ACCOUNT UNLOCK;可以主动解锁。这两套机制不冲突:Connection Control 负责拖慢节奏,账号锁负责在合适时机一刀切断。生产上我一般让 Connection Control 先扛,账号锁只给高权限账号开。

5. 常见问题与排查实录

5.1 插件装了,却看不到任何 connection_control 参数

这是最高频的问题。大部分情况是只装了CONNECTION_CONTROL,没装CONNECTION_CONTROL_ADMIN。系统变量和视图都是由 ADMIN 插件提供的,核心插件只管监测和延迟。解决办法很简单,补上第二条INSTALL PLUGIN命令,再SHOW VARIABLES LIKE 'connection_control%'就能看到了。

5.2 INSTALL PLUGIN 报 1126,找不到共享库

ERROR 1126 (HY000): Can't open shared library 'connection_control.so'。先检查插件目录:

SHOW VARIABLES LIKE 'plugin_dir';

然后在系统里看文件在不在:

ls -l /usr/lib64/mysql/plugin/connection_control.so

文件不存在,就要检查是不是装的 MySQL 客户端而不是服务端,或者安装时裁剪了插件目录。在 RHEL/CentOS 上一般yum install mysql-server完整安装后就会带。如果文件存在但还是报错,可能是 mysql 进程用户没有读权限,或者目录被挂载成 noexec 导致无法加载,逐个排查这两点基本能解决。

5.3 延迟没有按预期出现

不是所有失败都会被计数。插件主要统计进入认证阶段后返回Access denied的失败;客户端在 TCP 建立阶段就断开、握手没完成、连接被防火墙直接 RST 这类情况,通常不会计入。另外计数是按user@host分开的,测试时如果一会儿用127.0.0.1、一会儿用localhost去连,会被当成两个账号分别计数。还有一点经常被忽略:实例重启之后计数全部清零,如果你重启过,就理解为什么延迟消失了。最后再确认下min_connection_delay单位是毫秒,别把 1000 想当然写成 1。

5.4 会不会误伤正常用户和应用连接池

会,而且很容易。之前我讲过默认阈值是 3,一个同事手滑输错三次,第四次就会被拖 1 秒;应用连接池如果配置的密码错误,启动时它会高频重试,这时候整个池子都会陷入“连不上、等待、再连”的循环,应用启动时间暴涨。排查办法是看这两个值:

SHOW GLOBAL STATUS WHERE VARIABLE_NAME IN ('Aborted_connects', 'Connection_control_delay_generated');

如果Aborted_connects和delay_generated双高,说明确实有大量失败认证。应急处理可以先把阈值临时改大甚至关闭延迟:

SET GLOBAL connection_control_failed_connections_threshold = 100;

等应用修复密码后再调回正常值。生产环境的建议是:应用账号和管理员账号分开策略,应用账号阈值设高一点,管理员账号另外通过内网和防火墙做访问控制。

5.5 主从复制账号会影响吗

正常情况下不会。只要复制账号密码正确,主从复制不涉及失败认证,插件对复制链路毫无感知。但如果你改了复制账号密码忘了同步到从库,或者复制账号被误删,IO 线程反复连接失败,它同样会被计数并延迟,表现就是错误日志里一直刷Access denied,复制延迟持续拉大,且从库重连间隔越来越长。处理顺序是:先改对密码、恢复复制,再考虑是否需要重启实例清掉计数。

5.6 常见问题速查表

现象可能原因处理办法
查不到 connection_control 参数只装了核心插件没装 ADMIN补INSTALL PLUGIN CONNECTION_CONTROL_ADMIN
INSTALL 报 1126插件文件不存在/权限问题检查 plugin_dir,文件缺失则补装完整 mysql-server
延迟没生效阈值设为 0 / host 不一致 / 重启过确认参数,统一用相同主机名连接
应用连接池启动超时密码配置错误触发延迟修密码,临时调高阈值
主从延迟增大且报 Access denied复制账号密码错误改密码后重启实例清计数
多 IP 轮番尝试不触发单账号单 IP 计数分散配合 fail2ban 或云安全组封来源
想手动清空计数无直接清理命令该账号成功登录一次或重启实例

6. 我的调优建议与一点体会

根据遇到的场景,我给一个可以直接抄的基线配置。内网有堡垒机、访问走固定跳板的环境,threshold=3、min=1000、max=30000是比较舒服的组合,既能拦住爆破,又基本不影响正常使用。3306 有一定暴露面、测试环境账号乱的环境,建议threshold=5、min=1000、max=60000,必要时把failed_connections_limit设成 8 到 10,但要保证运维账号是独立账号,别跟应用共用。

日常监控也别只看业务指标,把这两个状态变量加进告警里:

SHOW GLOBAL STATUS WHERE VARIABLE_NAME IN ('Aborted_connects', 'Connection_control_delay_generated');

一旦Connection_control_delay_generated在短时间内快速增长,基本就是有人在批量试密码。这时候配合错误日志里的Access denied for user来源 IP,去防火墙或安全组封掉来源,才是一个完整的处置闭环。

最后说点个人体会。我早期把阈值设成 2、最小延迟设成 5000ms,觉得越敏感越好,结果开发同事连着输错两次,把测试环境的应用账号拖进了延迟窗口,一早上全是连接超时告警,排查了大半天才意识到是插件在起作用。后来我把规矩定成:阈值不低于 5,最大延迟一定要设置,所有生产改动都要先检查是否会误伤“自己人”。安全策略最重要的是别把自己锁死,Connection Control 的“延迟而不是锁死”本身就是这个思路,用它的时候,也应该用它自己的逻辑来约束我们的配置习惯。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询