编程 ERROR 2061 不是密码错:caching_sha2_password 缓存失效后,非 TLS 连接全掉进 full auth

2026-11-11 00:03:50

现象:ERROR 2061 “requires secure connection”:根因是 caching_sha2_password 缓存未命中

现象

非 TLS 连接 MySQL 8.x,账号使用默认插件 caching_sha2_password,连接时报:

ERROR 2061 (HY000): Authentication plugin 'caching_sha2_password' reported error: Authentication requires secure connection.

同一条命令在别的机器、别的时间点又能正常连接。JDBC 下对应的报错是 Public Key Retrieval is not allowed,canal 之类的客户端下是 caching_sha2_password Auth failed。

协议定义参考 MySQL 官方协议文档 与 官方手册 8.4.1.2。

相关量的定义:

  • Nonce = 20 字节随机数
  • Scramble = XOR(SHA256(password), SHA256(SHA256(SHA256(password)), Nonce))
  • Hash entry = account_name -> SHA256(SHA256(user_password))
  • authentication_string 存的是 SHA256-crypt 5000 轮结果($A$ 前缀)

两阶段认证:fast 与 full

Fast authentication:服务端内存缓存里已有该账号的 hash entry,用客户端发来的 scramble 直接校验,通过即进入命令阶段。

Complete / full authentication:缓存没有条目或校验失败,服务端要求走完整认证——通过安全通道(TLS / Unix socket / shared memory),或用 RSA 加密把密码送上来。服务端用 mysql.user.authentication_string 校验,成功后把 hash entry 写入缓存。

包层面:握手后服务端可能回 AuthMoreData,status byte 为 0x01,其中:

  • 0x03 = fast_auth_success,紧随 OK 包
  • 0x04 = perform_full_authentication,要求客户端继续发密码

另有 AuthSwitchRequest(0xfe + plugin name),用于握手时声明的插件与账号实际插件不一致的情况。

缓存失效时机

缓存由 sha2_cache_cleaner 审计插件维护,内置无需安装。key 是 user@host,纯内存,不落盘。

事件说明
账号创建后首次连接无 hash entry,必然 full auth
改密(ALTER USER / SET PASSWORD)条目失效
RENAME USER条目失效
DROP USER条目失效
FLUSH PRIVILEGES清空全部账号缓存
服务端关闭缓存不跨重启

以上事件之后,该账号的下一次连接必须使用安全连接或 RSA 密钥交换;成功之后重新进缓存,直到下一次失效事件。

由此产生的三个真实症状

  1. 手动登一次就好:运维先用带 --get-server-public-key 的 mysql 客户端连一次,缓存里写入了 hash entry,业务连接随后命中 fast auth。
  2. FLUSH PRIVILEGES 后批量 2061:清空的是全部账号缓存,所有非 TLS 业务连接同时掉进 full auth。
  3. 重启后首批连接失败:缓存不跨重启,重启后的头几条连接全部走 full auth。

非 TLS 下的 RSA 公钥路径

服务端默认不主动下发公钥,客户端没带公钥就报 2061。客户端选项有两种:

  • --get-server-public-key:向服务端索取,方便,但存在中间人替换公钥的风险
  • --server-public-key-path=file:预先配置公钥文件,推荐

服务端侧变量:

  • caching_sha2_password_private_key_path / caching_sha2_password_public_key_path
  • caching_sha2_password_auto_generate_rsa_keys 默认开启自动生成,文件在 datadir 下 private_key.pem / public_key.pem(社区文章里这两个名字常被写反)
  • 状态变量 Caching_sha2_password_rsa_public_key 可读当前公钥

复制链路:CHANGE REPLICATION SOURCE TO SOURCE_PUBLIC_KEY_PATH='...' 或 GET_SOURCE_PUBLIC_KEY=1;MGR 在 group_replication_ssl_mode=DISABLED 时需要 group_replication_recovery_get_public_key=ON 或 group_replication_recovery_public_key_path。

各驱动差异

  • JDBC Connector/J 8.x:allowPublicKeyRetrieval 默认 false,非 TLS + 未缓存时报 Public Key Retrieval is not allowed,需要显式打开。
  • go-sql-driver/mysql 1.5.0+:已实现 caching_sha2_password 完整认证,非 TLS 时自动向服务端索取公钥,不需要 JDBC 那样的开关;也可以用 tls=true / preferred 直接走 TLS 绕开 RSA。
  • PHP PDO(mysqlnd):7.4+ 支持该插件。
  • 自研协议客户端(各种 binlog 同步、代理、中间件):必须自己把 AuthMoreData 和后续 OK 包读完,没有库帮忙兜底。

自研协议客户端的包对齐坑

krowinski/php-mysql-replication PR #148:MySQL 8.4 + caching_sha2_password 账号下,switchAuth() 读完 auth-switch 响应后没有把 caching_sha2_password 多发的包抽干(AuthMoreData 0x01+0x03、随后 OK),导致后续每次 getResponse() 都提前两个包。第一个读到的包被当成 event 交给 Event::consume(),在 readInt32() / readUInt16() 上抛 TypeError(PHP 8 严格类型)。

修法是抽干这两个包,遇到 0x04 直接抛出明确异常而不是挂住,并给 consume() 加上小于 20 字节的最小包头保护。

另一个案例:alibaba/canal PR #4767。canal 1.1.6 + MySQL 8.0.30 只实现了 fast auth,没有实现 full auth 的公钥获取,所以「首次连接」或执行 FLUSH PRIVILEGES 之后稳定报 caching_sha2_password Auth failed;非首次连接因为命中缓存反而正常。PR 补上了 full auth 从服务端获取 public key 的流程。

排查顺序

  1. 看报错落在哪一类:2061 / Public Key Retrieval is not allowed / caching_sha2_password Auth failed,都指向缓存未命中的 full auth。
  2. 确认连接方式:是否 TLS、是否 Unix socket、是否 shared memory。安全通道下不会触发 RSA 路径。
  3. 对照失效表:是不是刚新建账号、改密、RENAME USER、FLUSH PRIVILEGES 或刚重启。
  4. 命令行加 --get-server-public-key 或 --server-public-key-path;JDBC 加 allowPublicKeyRetrieval=true;go-sql-driver 确认版本 ≥ 1.5.0。
  5. 自研客户端检查 AuthMoreData 与后续 OK 包是否抽干净,包偏移是否错位。
  6. 复制与 MGR 链路单独检查 SOURCE_PUBLIC_KEY_PATH / GET_SOURCE_PUBLIC_KEY / group_replication_recovery_get_public_key。

MySQL 8.0.4 起默认插件从 mysql_native_password 改为 caching_sha2_password,native 在 8.0.34 弃用、8.4 默认不加载、9.0 服务端移除,拿它兜底是死路。

推荐文章

程序员茄子在线接单