MySQL的密码复杂度策略和登录失败锁定机制,是数据库安全配置中最基础也最容易出现误区的两个环节。很多运维人员要么完全没开,要么配置不当导致正常用户频繁被锁,要么以为开了就万事大吉,实际上绕过的方式比想象中多。下面直接拆解这两个机制的具体配置方法、参数含义、常见坑点以及组合使用的最佳实践。

密码复杂度策略:validate_password组件

MySQL从5.6版本开始提供validate_password插件,5.7和8.0版本将其作为组件内置,但默认并不启用。要启用它,先在MySQL命令行执行以下检查,确认插件是否已加载:

SHOW VARIABLES LIKE 'validate_password%';

如果返回空结果,说明未启用。启用方式有两种,一是在运行时动态安装:

INSTALL COMPONENT 'file://component_validate_password';

二是在配置文件my.cnf中写入,让MySQL启动时自动加载:

[mysqld]
plugin-load-add=validate_password.so

注意,MySQL 8.0推荐使用COMPONENT方式而非PLUGIN方式,因为组件架构是8.0的新特性,卸载和升级更灵活。启用后,再用SHOW VARIABLES查看,会看到一系列以validate_password开头的系统变量。这几个参数直接决定了密码策略的严格程度,逐一说明:

validate_password.policy:策略级别,取值LOW、MEDIUM、STRONG。LOW只检查长度;MEDIUM额外要求至少包含数字、大小写字母和特殊字符中的一种;STRONG则在MEDIUM基础上,还会检查密码是否与字典文件中的单词匹配。生产环境建议至少设为MEDIUM,STRONG需要额外配置字典文件路径。

validate_password.length:密码最小长度。默认值通常是8,但这个长度在2025年的算力水平下已经不够安全,建议至少设为12到16位。注意这个参数受policy级别影响,如果policy是MEDIUM或STRONG,实际生效的长度是max(length, 各字符类型数量之和)。

validate_password.mixed_case_count:要求密码中大小写字母的最少数量。设为1意味着至少包含一个大写和一个小写字母。

validate_password.number_count:要求密码中最少数字个数。

validate_password.special_char_count:要求密码中最少特殊字符个数。

validate_password.dictionary_file:仅在policy为STRONG时生效,指向一个字典文件路径,MySQL会检查密码是否与该文件中的任何单词匹配。这个文件需要手动创建,每行一个单词,可以用常见的弱密码列表。

动态修改这些参数用SET GLOBAL命令,例如:

SET GLOBAL validate_password.policy = STRONG;
SET GLOBAL validate_password.length = 14;
SET GLOBAL validate_password.mixed_case_count = 2;
SET GLOBAL validate_password.number_count = 2;
SET GLOBAL validate_password.special_char_count = 2;

修改后立即生效,但仅对新创建的密码起作用,已存在的用户密码不受影响。要让现有用户在下一次修改密码时也遵循新策略,需要配合密码过期策略强制他们修改。

这里有一个容易被忽略的点:validate_password只对通过SQL语句创建或修改用户密码的操作生效,比如CREATE USER、ALTER USER、SET PASSWORD。如果你直接通过UPDATE语句修改mysql.user表,这个插件完全不会拦截。所以必须严格控制mysql库的写权限,禁止应用账号直接操作系统表。

登录失败锁定:connection_control组件

登录失败锁定在MySQL 8.0中由connection_control组件提供,5.7版本则是connection_control插件。这个组件的作用是:当一个账号连续登录失败达到一定次数后,后续的登录尝试会被延迟响应,延迟时间随失败次数递增,从而有效阻断暴力破解。

启用方式与validate_password类似,运行时安装:

INSTALL COMPONENT 'file://component_connection_control';

或者在配置文件中加入:

[mysqld]
connection-control=FORCE_PLUS_PERMANENT
connection-control-failed-login-attempts=FORCE_PLUS_PERMANENT

安装后,通过SHOW VARIABLES查看相关参数:

connection_control.failed_connections_threshold:允许的连续登录失败次数,默认为3。达到这个阈值后,账号的后续登录请求会被延迟处理。

connection_control.min_connection_delay:最小延迟时间,单位毫秒,默认1000毫秒。

connection_control.max_connection_delay:最大延迟时间,单位毫秒,默认2147483647毫秒(约24.8天)。实际延迟时间会随着失败次数线性增长,直到达到这个上限。

延迟计算公式大致为:min_delay * (失败次数 - threshold + 1),结果不超过max_delay。举例来说,threshold设为3,min_delay设为1000毫秒,第4次失败后延迟1秒,第5次失败后延迟2秒,以此类推。

这里有一个关键细节:connection_control并不会真正“锁定”账号,而是通过增加响应延迟来拖慢攻击者的节奏。这与Oracle或SQL Server那种“输错N次锁定账号,需要管理员手动解锁”的模式不同。MySQL这种设计的好处是避免了因攻击导致正常用户被永久锁定的风险,但坏处是如果攻击者足够有耐心,理论上仍可以继续尝试,只不过速度被极大压制。

要查看当前哪些账号处于失败累计状态,可以查询information_schema中的connection_control_failed_login_attempts表:

SELECT * FROM information_schema.connection_control_failed_login_attempts;

这个表会显示每个用户当前累计的失败次数。当一次登录成功后,该用户的失败计数会归零。这意味着攻击者如果偶尔猜对一次密码,计数器就会重置,这是一个需要注意的风险点。

两个机制的组合策略与坑点

密码复杂度策略和登录失败锁定看起来是独立的功能,但在实际运维中需要协同配置,否则会出现几种典型的失效场景。

第一,密码复杂度策略设得太松,登录失败锁定形同虚设。如果用户密码都是“12345678”或“password”,攻击者用最常见的几个弱密码就能在锁定触发前直接命中。所以validate_password.policy至少设为MEDIUM,length至少12位,这是底线。

第二,登录失败阈值设得太低,正常用户频繁被延迟。threshold设为3看起来很安全,但实际场景中,运维人员可能因为键盘布局、大小写锁定、忘记密码等原因连续输错几次,然后就被延迟搞得焦躁。建议threshold设为5到10,同时把min_delay设得稍高一些,比如3000毫秒,这样既能给正常用户一定的容错空间,又能在暴力破解发生时有效拖慢攻击。

第三,应用连接池的账号需要特殊处理。很多应用使用连接池,当数据库密码变更后,如果应用侧没有同步更新,连接池会不断尝试用旧密码建立连接,短时间内产生大量失败登录,迅速触发锁定延迟,导致整个应用瘫痪。解决方法是:为应用账号单独设置较高的threshold,或者在变更密码前先更新应用配置,再修改数据库密码,确保两者同步。更稳妥的做法是使用MySQL 8.0的双密码功能,在过渡期内同时允许新旧两个密码:

ALTER USER 'app_user'@'%' IDENTIFIED BY 'new_password' RETAIN CURRENT PASSWORD;

这样应用可以逐步切换到新密码,期间旧密码仍然有效,不会产生登录失败。

第四,connection_control的延迟是针对每个客户端连接独立计算的,而不是全局锁定。这意味着攻击者可以并发发起大量连接,每个连接独立计数,虽然每个连接都会被延迟,但并发量足够大时仍然能维持可观的尝试速率。要应对这种情况,需要在网络层或操作系统层配合使用fail2ban之类的工具,根据MySQL错误日志中的拒绝记录来封禁IP。

第五,MySQL的错误日志会记录每次登录失败的信息,包括尝试的用户名和来源IP。开启登录失败锁定后,日志量会显著增加,需要配置日志轮转和监控告警。建议将登录失败相关的日志单独采集分析,设置阈值告警,当某个账号的失败次数在短时间内异常升高时,安全团队能及时介入。

生产环境推荐配置

综合以上分析,给出一套适用于大多数生产环境的配置参考。在my.cnf中添加:

[mysqld]
# 密码复杂度策略
plugin-load-add=validate_password.so
validate_password.policy=STRONG
validate_password.length=14
validate_password.mixed_case_count=2
validate_password.number_count=2
validate_password.special_char_count=2
validate_password.dictionary_file=/etc/mysql/dictionary.txt

# 登录失败锁定
connection-control=FORCE_PLUS_PERMANENT
connection-control-failed-login-attempts=FORCE_PLUS_PERMANENT
connection_control.failed_connections_threshold=5
connection_control.min_connection_delay=3000
connection_control.max_connection_delay=86400000

# 密码过期策略,强制用户定期更新
default_password_lifetime=90

字典文件/etc/mysql/dictionary.txt需要手动创建,内容可以包含常见弱密码、公司名称、产品名称、年份等容易被猜到的词汇,每行一个。这个文件不需要太大,几百个高频弱密码就能显著提升STRONG策略的实际效果。

配置完成后重启MySQL,然后验证各项参数是否生效:

SHOW VARIABLES LIKE 'validate_password%';
SHOW VARIABLES LIKE 'connection_control%';

创建测试用户验证策略是否按预期工作:

CREATE USER 'test'@'localhost' IDENTIFIED BY 'weak';
-- 应该报错,提示密码不符合策略

这套配置在安全性上达到了较高水准,同时兼顾了运维的可用性。threshold设为5给了正常用户一定的容错空间,min_delay设为3秒能有效拖慢暴力破解,max_delay设为24小时防止延迟无限增长,密码过期90天则强制用户定期更换密码,形成完整的安全闭环。

最后提醒一点:任何安全策略都需要持续维护和审计。密码复杂度策略和登录失败锁定只是数据库安全的第一道防线,还需要配合最小权限原则、网络隔离、传输加密、审计日志等措施,才能构建真正可靠的数据库安全体系。定期检查information_schema中的失败登录记录,分析异常模式,及时调整策略参数,这些日常工作比一次性配置重要得多。