1. 问题现象与背景分析
最近在Java项目中连接MySQL 8.0数据库时,不少开发者遇到了"Public Key Retrieval is not allowed"的异常。这个错误通常发生在使用较新版本的MySQL Connector/J(8.0+)时,特别是在以下场景:
- 从Java应用通过JDBC连接MySQL 8.0+服务器
- 使用了caching_sha2_password认证插件(MySQL 8.0默认)
- 未正确配置SSL连接参数
错误堆栈会显示类似这样的信息:
code复制java.sql.SQLNonTransientConnectionException: Public Key Retrieval is not allowed
at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:108)
...
2. 问题根源深度解析
2.1 MySQL 8.0认证机制的变化
MySQL 8.0将默认的身份验证插件从mysql_native_password更改为caching_sha2_password。这一变化带来了更强的安全性,但也引入了新的连接行为:
- 密码交换机制:当使用caching_sha2_password时,客户端需要从服务器获取RSA公钥来加密密码
- 安全限制:默认情况下,MySQL Connector/J不允许自动获取公钥,以防止潜在的中间人攻击
- SSL的影响:如果启用了SSL加密连接,公钥可以通过安全通道传输;但非SSL连接时则需要显式允许公钥获取
2.2 连接过程中的关键交互
让我们看看连接时发生了什么:
- 客户端发起连接请求
- 服务器告知使用caching_sha2_password认证
- 客户端需要加密密码,但缺少服务器公钥
- 默认配置下,客户端不允许请求公钥(安全考虑)
- 连接失败,抛出"Public Key Retrieval is not allowed"
3. 解决方案与配置实践
3.1 基础解决方案:允许公钥获取
最简单的解决方法是在JDBC连接URL中添加参数:
java复制jdbc:mysql://localhost:3306/db?allowPublicKeyRetrieval=true
或者在DataSource配置中设置:
java复制dataSource.setAllowPublicKeyRetrieval(true);
注意:这种方法虽然简单,但在生产环境中应谨慎使用,特别是在非SSL连接时
3.2 更安全的解决方案:使用SSL连接
推荐的安全实践是配置SSL连接:
java复制jdbc:mysql://localhost:3306/db?useSSL=true&allowPublicKeyRetrieval=true
或者通过DataSource:
java复制dataSource.setUseSSL(true);
dataSource.setAllowPublicKeyRetrieval(true);
3.3 替代方案:更改认证插件
如果不方便修改代码,可以在MySQL服务器端更改用户认证方式:
sql复制ALTER USER 'username'@'host' IDENTIFIED WITH mysql_native_password BY 'password';
4. 生产环境最佳实践
4.1 安全配置建议
- 优先使用SSL:始终在生产环境启用SSL加密
- 限制allowPublicKeyRetrieval:仅在必要时启用,且应配合SSL使用
- 连接池配置:如果使用连接池(如HikariCP),确保正确传递这些参数
4.2 典型连接字符串示例
java复制// 生产环境推荐配置
String url = "jdbc:mysql://localhost:3306/db?"
+ "useSSL=true&"
+ "allowPublicKeyRetrieval=true&"
+ "serverTimezone=UTC";
// 开发环境简化配置
String devUrl = "jdbc:mysql://localhost:3306/db?"
+ "allowPublicKeyRetrieval=true&"
+ "serverTimezone=UTC";
4.3 版本兼容性注意事项
- Connector/J 8.0+:必须处理此问题
- MySQL 5.7及以下:通常不会出现此问题
- 混合版本环境:确保所有环境使用一致的认证方式
5. 深入理解认证流程
5.1 caching_sha2_password工作原理
- 客户端发送连接请求
- 服务器发送随机盐值(salt)和认证方法
- 客户端需要:
- 要么通过SSL连接发送明文密码
- 要么使用RSA公钥加密密码
- 服务器验证密码
5.2 为什么需要公钥获取
在没有SSL的情况下:
- 客户端需要加密密码
- 加密需要服务器公钥
- 公钥需要从服务器获取
- 默认禁止自动获取(安全考虑)
6. 高级配置与故障排除
6.1 自定义公钥指定
如果不想自动获取公钥,可以手动指定:
java复制dataSource.setServerRSAPublicKeyFile("/path/to/public_key.pem");
6.2 常见错误排查
-
SSL配置问题:
- 确保MySQL服务器配置了SSL
- 检查Java信任库是否包含CA证书
-
时区问题:
- 总是明确设置serverTimezone参数
- 推荐使用UTC避免时区问题
-
认证插件不匹配:
- 检查用户使用的认证插件
- 确保客户端支持服务器使用的插件
6.3 性能考虑
- RSA密钥交换:会增加连接建立的耗时
- 连接池预热:考虑预先建立连接避免首次连接延迟
- SSL开销:现代硬件上SSL/TLS开销已很小
7. 实际案例与代码示例
7.1 完整DataSource配置
java复制public class SecureConnectionManager {
private static final String DB_URL = "jdbc:mysql://localhost:3306/mydb";
private static final String USER = "appuser";
private static final String PASS = "securepassword";
public static Connection getConnection() throws SQLException {
MysqlDataSource dataSource = new MysqlDataSource();
dataSource.setUrl(DB_URL);
dataSource.setUser(USER);
dataSource.setPassword(PASS);
// 安全配置
dataSource.setUseSSL(true);
dataSource.setAllowPublicKeyRetrieval(true);
dataSource.setServerTimezone("UTC");
// 性能配置
dataSource.setCachePrepStmts(true);
dataSource.setPrepStmtCacheSize(250);
dataSource.setPrepStmtCacheSqlLimit(2048);
return dataSource.getConnection();
}
}
7.2 Spring Boot配置示例
application.properties:
properties复制spring.datasource.url=jdbc:mysql://localhost:3306/mydb?useSSL=true&allowPublicKeyRetrieval=true&serverTimezone=UTC
spring.datasource.username=appuser
spring.datasource.password=securepassword
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
8. 安全审计与风险评估
8.1 allowPublicKeyRetrieval的风险
- 中间人攻击:攻击者可能伪造公钥
- 密码泄露:加密强度依赖于RSA密钥长度
- 日志泄露:连接字符串可能被记录
8.2 缓解措施
- 强制SSL:始终结合useSSL=true使用
- 网络隔离:数据库服务器应位于安全网络区域
- 定期轮换密钥:定期更换MySQL的RSA密钥对
8.3 安全配置检查清单
- [ ] SSL/TLS加密已启用
- [ ] 使用强密码策略
- [ ] 限制数据库访问IP
- [ ] 定期更新Connector/J版本
- [ ] 监控异常连接尝试
9. 版本迁移与升级策略
9.1 从MySQL 5.7升级到8.0
- 测试认证兼容性:
sql复制SELECT user,host,plugin FROM mysql.user; - 逐步迁移用户:
sql复制ALTER USER 'appuser'@'%' IDENTIFIED WITH caching_sha2_password BY 'password'; - 客户端同步升级:
- 更新Connector/J到8.0+
- 测试所有连接场景
9.2 回滚方案
如果遇到兼容性问题:
sql复制-- 临时回退到旧认证方式
ALTER USER 'appuser'@'%' IDENTIFIED WITH mysql_native_password BY 'password';
-- 修改全局默认(不推荐长期使用)
SET GLOBAL default_authentication_plugin='mysql_native_password';
10. 性能优化与监控
10.1 连接建立性能
-
监控指标:
- 平均连接建立时间
- 认证失败率
- SSL握手时间
-
优化建议:
- 使用连接池减少连接建立次数
- 考虑保持长连接
- 优化SSL配置(如会话复用)
10.2 JVM配置建议
对于高并发应用:
code复制-Djavax.net.ssl.sessionCacheSize=20480
-Djavax.net.ssl.sessionTimeout=300
10.3 MySQL服务器配置
my.cnf优化:
ini复制[mysqld]
# RSA密钥缓存
caching_sha2_password_auto_generate_rsa_keys=ON
caching_sha2_password_private_key_path=private_key.pem
caching_sha2_password_public_key_path=public_key.pem
# SSL配置
ssl-ca=ca.pem
ssl-cert=server-cert.pem
ssl-key=server-key.pem
11. 多环境配置管理
11.1 开发vs生产配置
建议使用环境变量管理差异:
java复制String useSSL = System.getenv("DB_USE_SSL") != null ?
System.getenv("DB_USE_SSL") : "false";
String jdbcUrl = String.format(
"jdbc:mysql://%s:%s/%s?useSSL=%s&allowPublicKeyRetrieval=%s",
System.getenv("DB_HOST"),
System.getenv("DB_PORT"),
System.getenv("DB_NAME"),
useSSL,
useSSL.equals("true") ? "true" : "false"
);
11.2 配置中心集成
与Spring Cloud Config等集成示例:
yaml复制# application-prod.yml
spring:
datasource:
url: jdbc:mysql://${DB_HOST}:3306/${DB_NAME}?useSSL=true&allowPublicKeyRetrieval=true
hikari:
connection-init-sql: "SET time_zone='+00:00'"
12. 容器化部署考虑
12.1 Docker环境配置
示例docker-compose.yml片段:
yaml复制services:
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: rootpass
MYSQL_USER: appuser
MYSQL_PASSWORD: userpass
volumes:
- ./mysql/conf.d:/etc/mysql/conf.d
- ./mysql/ssl:/etc/mysql/ssl
command:
--require-secure-transport=ON
--ssl-ca=/etc/mysql/ssl/ca.pem
--ssl-cert=/etc/mysql/ssl/server-cert.pem
--ssl-key=/etc/mysql/ssl/server-key.pem
12.2 Kubernetes配置
ConfigMap示例:
yaml复制apiVersion: v1
kind: ConfigMap
metadata:
name: mysql-config
data:
my.cnf: |
[client]
ssl-mode=REQUIRED
ssl-ca=/etc/mysql/ssl/ca.pem
ssl-cert=/etc/mysql/ssl/client-cert.pem
ssl-key=/etc/mysql/ssl/client-key.pem
13. 测试策略与验证
13.1 连接测试用例
JUnit测试示例:
java复制@Test
public void testDatabaseConnection() {
try (Connection conn = ConnectionManager.getConnection()) {
assertTrue(conn.isValid(1000));
// 验证SSL是否实际启用
try (Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SHOW STATUS LIKE 'Ssl_cipher'")) {
assertTrue(rs.next());
assertNotNull(rs.getString(2));
}
} catch (SQLException e) {
fail("Connection failed: " + e.getMessage());
}
}
13.2 安全审计测试
- SSL/TLS验证:
bash复制
openssl s_client -connect mysqlserver:3306 -starttls mysql - 协议与加密套件检查:
bash复制
nmap --script mysql-info,mysql-vuln-cve2012-2122 -p 3306 mysqlserver
14. 替代方案评估
14.1 其他JDBC参数
-
useSSL与requireSSL的区别:
useSSL=true:尝试使用SSLrequireSSL=true:强制SSL连接
-
verifyServerCertificate:
- 设置为false可跳过证书验证(仅限测试环境)
14.2 不同认证插件对比
| 插件 | 安全性 | 性能 | 兼容性 |
|---|---|---|---|
| caching_sha2_password | 高 | 中 | MySQL 8.0+ |
| mysql_native_password | 中 | 高 | 所有版本 |
| sha256_password | 高 | 低 | 5.6+ |
15. 长期维护建议
-
版本升级计划:
- 定期检查MySQL和Connector/J更新
- 测试新版本兼容性
-
安全补丁监控:
- 订阅MySQL安全公告
- 关注CVE数据库相关漏洞
-
文档维护:
- 记录所有数据库连接配置
- 更新团队知识库
-
定期审计:
- 检查实际使用的认证方法
- 验证SSL配置有效性
- 审查连接参数安全性
