就是喜欢美食 發表於 2025-5-12 09:45:00

Java应用出现 Public Key Retrieval is not allowed 报错的常见原因和解决方法

<h1 data-tool="mdnice编辑器"><span style="color: rgba(0, 128, 0, 1)">问题现象</span></h1>
<p data-tool="mdnice编辑器"><span>Java 应用在运行过程中突然报<code><span>java.sql.SQLNonTransientConnectionException: Public Key Retrieval is not allowed</span></code><span>错误。</span></span></p>
<p data-tool="mdnice编辑器"><span>开发童鞋表示不理解,毕竟应用没做任何变更,为什么会突然出现这个错误?</span></p>
<pre data-tool="mdnice编辑器"><span data-cacheurl="" data-remoteid="" data-lazy-bgimg="https://mmbiz.qpic.cn/mmbiz_svg/BO1qQiajiacVll2lZZ7QEHpaGp010vNgdTZpC499x6fDYD6IXjhHscicX7TJgibQIzLdFWHKwTys7BC9a67BBh02ODoU0EeWoEDp/640?wx_fmt=svg&amp;from=appmsg" data-fail="0"><code>2025<span>-03<span>-31<span>&nbsp;08<span>:31<span>:11<span>&nbsp;- create connection SQLException, url: jdbc:mysql://10.0.1.86:3306/information_schema?useSSL=false, errorCode 0, state 08001<span><br><span>java.sql.SQLNonTransientConnectionException: Public Key Retrieval is not allowed<span><br><span>&nbsp; &nbsp; &nbsp; &nbsp; at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:110<span>)<span><br><span>&nbsp; &nbsp; &nbsp; &nbsp; at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:122<span>)<span><br><span>&nbsp; &nbsp; &nbsp; &nbsp; at com.mysql.cj.jdbc.ConnectionImpl.createNewIO(ConnectionImpl.java:828<span>)<span><br><span>&nbsp; &nbsp; &nbsp; &nbsp; at com.mysql.cj.jdbc.ConnectionImpl.&lt;init&gt;(ConnectionImpl.java:448<span>)<span><br><span>&nbsp; &nbsp; &nbsp; &nbsp; at com.mysql.cj.jdbc.ConnectionImpl.getInstance(ConnectionImpl.java:241<span>)<span><br><span>&nbsp; &nbsp; &nbsp; &nbsp; at com.mysql.cj.jdbc.NonRegisteringDriver.connect(NonRegisteringDriver.java:198<span>)<span><br><span>&nbsp; &nbsp; &nbsp; &nbsp; at com.alibaba.druid.pool.DruidAbstractDataSource.createPhysicalConnection(DruidAbstractDataSource.java:1682<span>)<span><br><span>&nbsp; &nbsp; &nbsp; &nbsp; at com.alibaba.druid.pool.DruidAbstractDataSource.createPhysicalConnection(DruidAbstractDataSource.java:1803<span>)<span><br><span>&nbsp; &nbsp; &nbsp; &nbsp; at com.alibaba.druid.pool.DruidDataSource$CreateConnectionThread.run(DruidDataSource.java:2914<span>)<span><br><span>&nbsp; &nbsp; &nbsp; &nbsp; ...<span><br></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></code></span></pre>
<h1 data-tool="mdnice编辑器"><span style="color: rgba(0, 128, 0, 1)">问题原因</span></h1>
<p data-tool="mdnice编辑器"><span>用户使用的密码认证插件是 caching_sha2_password 且 JDBC 连接串中指定了<code><span>useSSL=false</span></code><span>。</span></span></p>
<p data-tool="mdnice编辑器"><span>当碰到以下场景,就会出现上述报错:</span></p>
<ol class="list-paddingleft-1">
<li><span>第一次连接时。</span></li>
<li><span>数据库实例发生了重启。</span></li>
<li><span>服务端执行了 FLUSH PRIVILEGES 操作。</span></li>
</ol>
<h1 data-tool="mdnice编辑器"><span style="color: rgba(0, 128, 0, 1)">解决方法</span></h1>
<p data-tool="mdnice编辑器"><span>解决方法有以下几种:</span></p>
<p data-tool="mdnice编辑器"><strong>方案 1</strong></p>
<p data-tool="mdnice编辑器"><span>将用户的密码认证插件调整为 mysql_native_password。不推荐该方法,因为 MySQL 9.0 移除了 mysql_native_password。</span></p>
<pre data-tool="mdnice编辑器"><span data-cacheurl="" data-remoteid="" data-lazy-bgimg="https://mmbiz.qpic.cn/mmbiz_svg/BO1qQiajiacVll2lZZ7QEHpaGp010vNgdTZpC499x6fDYD6IXjhHscicX7TJgibQIzLdFWHKwTys7BC9a67BBh02ODoU0EeWoEDp/640?wx_fmt=svg&amp;from=appmsg" data-fail="0"><code>ALTER<span>&nbsp;USER<span>&nbsp;'u1'<span>@'%'<span>&nbsp;IDENTIFIED<span>&nbsp;WITH<span>&nbsp;mysql_native_password&nbsp;BY<span>&nbsp;'MySQL@2025'<span>;<span><br></span></span></span></span></span></span></span></span></span></code></span></pre>
<p data-tool="mdnice编辑器"><strong>方案 2</strong></p>
<p data-tool="mdnice编辑器"><span>在 JDBC 连接串中设置<code><span>useSSL=true</span></code><span>。推荐该方法,但开启 SSL 会有一定的性能开销。</span></span></p>
<p data-tool="mdnice编辑器"><span>如果既不想开启 SSL,又想避免 Public Key Retrieval is not allowed 错误,以下是两个可选的解决方案:</span></p>
<p data-tool="mdnice编辑器"><strong>方案 3</strong></p>
<p data-tool="mdnice编辑器"><span>在 JDBC 连接串中添加<code><span>allowPublicKeyRetrieval=true</span></code><span>。</span></span></p>
<p data-tool="mdnice编辑器"><span>该方法会自动从 MySQL 服务端获取 RSA 公钥,但这种方法有一定的安全风险,可能会受到中间人攻击。攻击者可以伪造 RSA 公钥,窃取用户密码。</span></p>
<p data-tool="mdnice编辑器"><strong>方案 4</strong></p>
<p data-tool="mdnice编辑器"><span>在 JDBC 连接串中指定<code><span>serverRSAPublicKeyFile</span></code><span>。</span></span></p>
<p data-tool="mdnice编辑器"><span>该方法需要将方案 3 中的公钥内容写到应用侧的一个本地文件中,具体步骤如下:</span></p>
<ul class="list-paddingleft-1">
<li>
<p><span>通过<code><span>show status like 'Caching_sha2_password_rsa_public_key'</span></code><span>命令或者从参数 caching_sha2_password_public_key_path(默认是 public_key.pem)指定的文件中获取公钥内容。</span></span></p>
</li>
<li>
<p><span>将公钥内容保存到应用侧的一个本地文件中。</span></p>
</li>
<li>
<p><span>在 JDBC 连接串中指定该公钥文件路径。如,</span></p>
</li>
</ul>
<pre data-tool="mdnice编辑器"><span data-cacheurl="" data-remoteid="" data-lazy-bgimg="https://mmbiz.qpic.cn/mmbiz_svg/BO1qQiajiacVll2lZZ7QEHpaGp010vNgdTZpC499x6fDYD6IXjhHscicX7TJgibQIzLdFWHKwTys7BC9a67BBh02ODoU0EeWoEDp/640?wx_fmt=svg&amp;from=appmsg" data-fail="0"><code><span>JDBC_URL =&nbsp;"jdbc:mysql://10.0.1.86:3306/information_schema?useSSL=false&amp;serverRSAPublicKeyFile=/usr/local/caching_sha2_password_public_key.pem"<span><br></span></span></code></span></pre>
<p data-tool="mdnice编辑器"><span>相较于方案 3,方案 4 无疑会更安全,但在高可用环境下并不适用,因为主从节点的公钥内容通常不同。一旦发生主从切换,JDBC 客户端在重新连接新的主节点时,就可能因公钥不一致而触发 Public Key Retrieval is not allowed 错误。除非在部署时显式配置,让主从节点使用相同的公钥,才能避免该问题。</span></p>
<h2 data-tool="mdnice编辑器"><span style="color: rgba(0, 128, 0, 1)">四种方案的优缺点对比</span></h2>
<span><span><br></span></span>
<table>
<thead>
<tr><th><span>方案</span></th><th><span>安全等级</span></th><th><span>适用场景</span></th><th><span>备注</span></th></tr>

</thead>
<tbody>
<tr>
<td><span>SSL 加密连接 (useSSL=true)</span></td>
<td><span>★★★★★</span></td>
<td><span>所有生产环境</span></td>
<td><span>最安全方案,推荐优先使用</span></td>

</tr>
<tr>
<td><span>手动配置 RSA 公钥 (serverRSAPublicKeyFile)</span></td>
<td><span>★★★★☆</span></td>
<td><span>禁用 SSL 的生产环境</span></td>
<td><span>需维护公钥文件</span></td>

</tr>
<tr>
<td><span>自动获取 RSA 公钥 (allowPublicKeyRetrieval=true)</span></td>
<td><span>★★☆☆☆</span></td>
<td><span>可信内网环境</span></td>
<td><span>存在中间人攻击风险</span></td>

</tr>
<tr>
<td><span>更改认证插件</span></td>
<td><span>★☆☆☆☆</span></td>
<td><span>不推荐</span></td>
<td><span>兼容性差,安全性低</span></td>

</tr>

</tbody>

</table>
<h1 data-tool="mdnice编辑器"><span style="color: rgba(0, 128, 0, 1)">根因分析</span></h1>
<p data-tool="mdnice编辑器"><span>简单来说,caching_sha2_password 插件为了加快认证过程,在服务端维护了一个<strong>密码哈希缓存</strong><span>。当客户端发起连接时:</span></span></p>
<ul class="list-paddingleft-1">
<li><span>如果用户的密码哈希已经被缓存,服务端可以直接验证,无需客户端发送明文密码进行验证。</span></li>
<li><span>如果缓存中没有该用户的密码哈希(比如第一次连接时,除此之外,数据库重启或执行 FLUSH PRIVILEGES,还会清除密码哈希缓存),则客户端需要发送明文密码进行认证。</span></li>

</ul>
<p data-tool="mdnice编辑器"><span>在发送明文密码时,出于安全考虑,MySQL 要求:</span></p>
<ul class="list-paddingleft-1">
<li><span>要么客户端和服务端之间建立 SSL 加密连接。</span></li>
<li><span>要么客户端允许通过服务端公钥加密明文密码。</span></li>

</ul>
<p data-tool="mdnice编辑器"><span>如果两者都不满足,就会抛出 Public Key Retrieval is not allowed 错误。</span></p>
<h1 data-tool="mdnice编辑器"><span style="color: rgba(0, 128, 0, 1)">caching_sha2_password 的认证交互流程</span></h1>
<p data-tool="mdnice编辑器"><span>以下是客户端与服务端使用 caching_sha2_password 插件进行身份认证时的完整交互流程:</span></p>
<p><img src="https://img2024.cnblogs.com/blog/576154/202505/576154-20250512094354040-1466809290.png" alt="" loading="lazy"></p>
<p>&nbsp;</p>
<br>
<p data-tool="mdnice编辑器"><span>具体实现细节如下:</span></p>
<ol class="list-paddingleft-1">
<li>
<p><span>MySQL 服务端收到客户端的请求后,会生成一个长度为 21 字节的随机数。</span></p>

</li>
<li>
<p><span>MySQL 服务端给客户端发送一个握手包(handshake packet)。</span></p>
<p><span>该握手包是 MySQL 服务端与客户端建立连接时发送的第一个数据包,包的内容如下:</span></p>
<p><span>握手包的构造和发送是在<code><span>send_server_handshake_packet</span></code><span>函数中实现的。</span></span></p>

</li>
<ol class="list-paddingleft-1">
<li>
<p><span>协议版本。</span></p>

</li>
<li>
<p><span>服务端版本。</span></p>

</li>
<li>
<p><span>线程 ID。</span></p>

</li>
<li>
<p><span>随机数的第一部分。</span></p>

</li>
<li>
<p><span>服务端能力标志(低16位) 。</span></p>

</li>
<li>
<p><span>默认字符集编号。</span></p>

</li>
<li>
<p><span>服务端状态标志。</span></p>

</li>
<li>
<p><span>服务端能力标志(高16位) 。</span></p>

</li>
<li>
<p><span>随机数的的长度。</span></p>

</li>
<li>
<p><span>保留字段。</span></p>

</li>
<li>
<p><span>随机数的第二部分。</span></p>

</li>
<li>
<p><span>默认的密码认证插件名称。</span></p>
<p><span>在 MySQL 8.4 之前,默认的密码认证插件由 default_authentication_plugin 参数决定。在 MySQL 5.7 中,该参数的默认值为 mysql_native_password,在 MySQL 8.0 中,该参数的默认值为 caching_sha2_password。在 MySQL 8.4 中,移除了这个参数,默认的密码认证插件硬编码为 caching_sha2_password。</span></p>

</li>

</ol>
<li>
<p><span>客户端在接收到服务端的握手包后,会根据握手包中的密码认证插件进行身份验证。对于 caching_sha2_password 插件,客户端将基于以下公式构造一个&nbsp;<code><span>scramble_response</span></code><span>,然后调用<code><span>prep_client_reply_packet</span></code><span>构造握手包发送给服务端。公式中的 random 是服务端在握手包中发送的随机数。</span></span></span></p>
<pre><span data-cacheurl="" data-remoteid="" data-lazy-bgimg="https://mmbiz.qpic.cn/mmbiz_svg/BO1qQiajiacVll2lZZ7QEHpaGp010vNgdTZpC499x6fDYD6IXjhHscicX7TJgibQIzLdFWHKwTys7BC9a67BBh02ODoU0EeWoEDp/640?wx_fmt=svg&amp;from=appmsg" data-fail="0"><code><span>scramble_response = XOR(SHA256(password), SHA256(SHA256(SHA256(password)) + random)<span><br></span></span></code></span></pre>
</li>
<li>
<p><span>服务端调用<code><span>parse_client_handshake_packet</span></code><span>解析客户端返回的握手包,解析出来的内容包括用户名、密码(不是明文密码,是 scramble_response)、库名。</span></span></p>
</li>
<li>
<p><span>基于用户名和客户端 IP 从<code><span>ACL</span></code><span>缓存中找到用户对应的认证信息,包括密码认证插件和密码哈希值。</span></span></p>
</li>
<li>
<p><span>调用<code><span>Caching_sha2_password::fast_authenticate</span></code><span>进行快速验证。快速验证的逻辑如下:</span></span></p>
</li>
<ul class="list-paddingleft-1">
<li>
<p><span>首先基于用户名和主机名构造一个 authorization_id。</span></p>
</li>
<li>
<p><span>判断 authorization_id 是否在 m_cache 存在。m_cache 即密码哈希缓存,是 caching_sha2_password 中的关键组件,它的底层实现是一个哈希表。该哈希表的键是 authorization_id,值是一个二维数组,用来存储新旧两个密码哈希值(从 MySQL 8.0 开始,一个用户可以设置两个密码),存储的值是对密码进行两次 SHA-256 哈希计算的结果,即<code><span>SHA256(SHA256(password)</span></code><span>。</span></span></p>
</li>
<li>
<p><span>如果存在,则通过以下步骤验证密码是否正确。</span></p>
<pre><span data-cacheurl="" data-remoteid="" data-lazy-bgimg="https://mmbiz.qpic.cn/mmbiz_svg/BO1qQiajiacVll2lZZ7QEHpaGp010vNgdTZpC499x6fDYD6IXjhHscicX7TJgibQIzLdFWHKwTys7BC9a67BBh02ODoU0EeWoEDp/640?wx_fmt=svg&amp;from=appmsg" data-fail="0"><code><span>SHA2(m_known, rnd) =&gt; scramble_stage1<span><br><span>XOR(scramble, scramble_stage1) =&gt; digest_stage1<span><br><span>SHA2(digest_stage1) =&gt; digest_stage2<span><br><span>m_known == digest_stage2<span><br></span></span></span></span></span></span></span></span></code></span></pre>
<p><span>其中,m_known 是 m_cache 中存储的密码哈希值,即 SHA256(SHA256(password),rnd 是服务端发送给客户端的随机数(random),scramble 是客户端返回给服务端的 scramble_response。</span></p>
<p><span>最后,将 m_known 与 digest_stage2 进行比较,如果相等,则意味着密码正确。这个时候,服务端会给客户端发送一个<code><span>fast_auth_success</span></code><span>包。</span></span></p>
</li>
<li>
<p><span>如果 authorization_id 在 m_cache 中不存在,或者存在但不匹配,则意味着快速验证失败,这个时候,服务端会给客户端发送一个<code><span>perform_full_authentication</span></code><span>包,要求客户端发送密码进行身份验证。</span></span></p>
</li>
</ul>
<li>
<p><span>客户端收到服务端发送的<code><span>perform_full_authentication</span></code><span>包后:</span></span></p>
</li>
<ul class="list-paddingleft-1">
<li>
<p><span>如果连接是安全的(即开启了 SSL),则会向客户端发送明文密码。</span></p>
</li>
<li>
<p><span>如果连接不是安全的(即没有开启 SSL,一般是因为客户端显式指定了<code><span>--ssl-mode=DISABLED</span></code><span>),则会调用 rsa_init() 初始化 RSA 公钥,这个公钥是 mysql 客户端参数 server-public-key-path 指定的。如果没指定,则看 mysql 客户端参数中是否指定了get-server-public-key。</span></span></p>
<p><span>如果既没指定 server-public-key-path,又没指定 get-server-public-key,则 mysql 客户端会提示<code><span>ERROR 2061 (HY000): Authentication plugin 'caching_sha2_password' reported error: Authentication requires secure connection.</span></code><span>错误。</span></span></p>
<p><span>JDBC 中的处理逻辑类似,对于非 SSL 连接,如果既没指定 serverRSAPublicKeyFile,又没指定allowPublicKeyRetrieval,则会提示<code><span>Public Key Retrieval is not allowed</span></code><span>错误,具体的实现细节可参考 CachingSha2PasswordPlugin.java 中的 nextAuthenticationStep 方法。</span></span></p>
<p><span>如果指定了 get-server-public-key,则客户端会向服务端发送一个<code><span>request_public_key</span></code><span>包。</span></span></p>
</li>
</ul>
<li>
<p><span>服务端收到客户端发送的<code><span>request_public_key</span></code><span>包后,会将 RSA 公钥发送给客户端。</span></span></p>
</li>
<li>
<p><span>客户端收到 RSA 公钥后,首先会将密码和服务端之前发送的随机数进行异或运算,然后使用 RSA 公钥对异或后的结果进行加密,最后将加密后的密文发送给服务端。</span></p>
</li>
<li>
<p><span>服务端收到加密后的密文后,会调用 decrypt_RSA_private_key 进行解密,获取明文密码。</span></p>
</li>
<li>
<p><span>服务端 Caching_sha2_password::authenticate 验证密码是否正确。验证的逻辑如下:</span></p>
</li>
<ul class="list-paddingleft-1">
<li>
<p><span>从 mysql.user 表 authentication_string 字段中提取迭代次数和盐值。</span></p>
<p><span>对于 caching_sha2_password,authentication_string 的格式如下:</span></p>
<pre><span data-cacheurl="" data-remoteid="" data-lazy-bgimg="https://mmbiz.qpic.cn/mmbiz_svg/BO1qQiajiacVll2lZZ7QEHpaGp010vNgdTZpC499x6fDYD6IXjhHscicX7TJgibQIzLdFWHKwTys7BC9a67BBh02ODoU0EeWoEDp/640?wx_fmt=svg&amp;from=appmsg" data-fail="0"><code><span>分隔符[摘要类型]分隔符[迭代次数]分隔符[盐值][哈希摘要]<span><br></span></span></code></span></pre>
<p><span>其中,分隔符默认是 $,摘要类型是单个字母,A 表示使用 SHA256 算法,迭代次数是 3 位十六进制字符串,默认是 005,乘以 ITERATION_MULTIPLIER(默认是 1000),即为实际迭代次数(默认是 5000 次)。盐值是一个长度为 20 的随机字符串,剩下的字符串即为哈希摘要,长度为 43 字节。</span></p>
</li>
<li>
<p><span>基于提取的迭代次数、盐值和明文密码生成一个哈希摘要。</span></p>
</li>
<li>
<p><span>判断提取的哈希摘要和生成的哈希摘要是否相等,如果相等,则意味着密码正确,否则是错误的。</span></p>
</li>
</ul>
</ol>
<h1 data-tool="mdnice编辑器"><span style="color: rgba(0, 128, 0, 1)">参考资料</span></h1>
<ol class="list-paddingleft-1">
<li>
<p><span>Protocol::HandshakeV10:https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_connection_phase_packets_protocol_handshake_v10.html</span></p>
</li>
<li>
<p><span>Caching_sha2_password information:https://dev.mysql.com/doc/dev/mysql-server/latest/page_caching_sha2_authentication_exchanges.html</span></p>
</li>
<li>
<p><span>WL#9591: Caching sha2 authentication plugin:https://dev.mysql.com/worklog/task/?id=9591</span></p>
</li>
</ol><br><br>
来源:https://www.cnblogs.com/ivictor/p/18872158
頁: [1]
查看完整版本: Java应用出现 Public Key Retrieval is not allowed 报错的常见原因和解决方法