Spring Boot中HikariCP连接池动态生成IAM密码的实现方案
问题描述
基于Spring Boot(2.3.5.RELEASE)和Spring Cloud(Hoxton.RELEASE)的项目,使用Hikari连接池搭配每15分钟过期的IAM令牌作为数据库密码。当前初始连接可正常使用生成的令牌,但连接池启动后新建的连接仍使用启动时的初始密码,导致令牌过期后新建连接失效。已将连接最大生命周期设为4小时,存活连接可正常工作,但新建连接无法获取新令牌。原定时任务方案因刷新间隔问题无法覆盖令牌过期后的新建连接需求,希望实现新建连接时自动使用最新密码,同时保留已建立的长连接。
解决方案
核心思路
利用HikariCP 3.4.0+(Spring Boot 2.3.5.RELEASE适配的HikariCP版本为3.4.5,支持该特性)提供的PasswordSupplier功能,让连接池每次创建新物理连接时,自动调用令牌生成逻辑获取最新IAM令牌,从根源解决新建连接使用过期密码的问题。
代码修改步骤
- 移除预创建DataSource的逻辑,改为让Hikari通过JDBC URL自行创建连接
- 配置对应数据库的JDBC连接地址
- 通过
setPasswordSupplier设置动态密码提供者,每次新建连接时自动获取最新令牌
修改后的DBConfig代码如下:
@Configuration public class DBConfig { private static final Logger logger = LoggerFactory.getLogger(DBConfig.class); private static final String SESSION_NAME = "db.session"; private static final int CONNECTION_IDLE_TIMEOUT = 1800000; // 最大空闲时间30分钟 private static final int CONNECTION_KEEP_ALIVE_TIME = 60000; // 每分钟保活一次 private static final int CONNECTION_MAX_LIFETIME = 14400000; // 连接最长存活4小时 @Value("${db.name}") private String dbName; @Value("${db.host}") private String host; @Value("${db.port}") private String port; @Value("${db.username}") private String username; @Value("${db.region}") private String awsRegion; @Value("${aws.role.arn}") private String awsRoleArn; @Bean(name = "getDataSource") public DataSource getDataSource() { logger.info("初始化DataSource连接池,配置信息: {}", this); try { HikariConfig hikariConfig = new HikariConfig(); // 构建JDBC URL(根据数据库类型调整,示例为MySQL) String jdbcUrl = String.format("jdbc:mysql://%s:%s/%s?useSSL=false&serverTimezone=UTC", host, port, dbName); hikariConfig.setJdbcUrl(jdbcUrl); hikariConfig.setUsername(username); // 设置动态密码供应商,每次新建连接时获取最新IAM令牌 hikariConfig.setPasswordSupplier(() -> { try { return IAMUtils.getIAMAuthToken(awsRegion, host, Integer.parseInt(port), username, awsRoleArn, SESSION_NAME); } catch (Exception e) { logger.error("获取IAM认证令牌失败", e); throw new RuntimeException("无法获取有效数据库连接令牌", e); } }); // 保留原有连接池配置 hikariConfig.setIdleTimeout(CONNECTION_IDLE_TIMEOUT); hikariConfig.setMaxLifetime(CONNECTION_MAX_LIFETIME); hikariConfig.setKeepaliveTime(CONNECTION_KEEP_ALIVE_TIME); return new HikariDataSource(hikariConfig); } catch (Exception e) { logger.error("DataSource初始化失败: {}", e.getMessage(), e); throw new RuntimeException("DataSource初始化异常", e); } } }
关键说明
- PasswordSupplier特性:HikariCP在每次创建新物理连接时,都会调用该Supplier获取最新密码,确保新建连接使用未过期的IAM令牌
- 长连接保留:已建立的连接无需重新认证,可继续使用原令牌(根据业务描述,数据库允许已连接会话在令牌过期后继续工作),无需调整
maxLifetime - 异常处理:在密码供应商中捕获令牌获取异常并抛出,确保连接池创建失败时快速暴露问题
低版本HikariCP替代方案
若使用的HikariCP版本低于3.4.0,可通过自定义DataSource包装HikariDataSource,在每次获取连接前检查并刷新令牌:
public class DynamicPasswordHikariDataSource extends HikariDataSource { private final String awsRegion; private final String host; private final int port; private final String username; private final String awsRoleArn; private final String sessionName; private long lastTokenRefreshTime = 0; private static final long TOKEN_REFRESH_INTERVAL = 14 * 60 * 1000; // 提前1分钟刷新令牌 public DynamicPasswordHikariDataSource(HikariConfig config, String awsRegion, String host, int port, String username, String awsRoleArn, String sessionName) { super(config); this.awsRegion = awsRegion; this.host = host; this.port = port; this.username = username; this.awsRoleArn = awsRoleArn; this.sessionName = sessionName; } @Override public Connection getConnection() throws SQLException { refreshTokenIfNeeded(); return super.getConnection(); } @Override public Connection getConnection(String username, String password) throws SQLException { refreshTokenIfNeeded(); return super.getConnection(username, password); } private void refreshTokenIfNeeded() { long now = System.currentTimeMillis(); if (now - lastTokenRefreshTime > TOKEN_REFRESH_INTERVAL) { synchronized (this) { if (now - lastTokenRefreshTime > TOKEN_REFRESH_INTERVAL) { try { String newToken = IAMUtils.getIAMAuthToken(awsRegion, host, port, username, awsRoleArn, sessionName); this.setPassword(newToken); this.softEvictConnections(); // 驱逐旧连接,强制新连接使用新密码 lastTokenRefreshTime = now; logger.info("已刷新IAM令牌并驱逐旧连接"); } catch (Exception e) { logger.error("刷新IAM令牌失败", e); } } } } } }
配置类中使用该包装类:
@Bean(name = "getDataSource") public DataSource getDataSource() { HikariConfig hikariConfig = new HikariConfig(); String jdbcUrl = String.format("jdbc:mysql://%s:%s/%s?useSSL=false&serverTimezone=UTC", host, port, dbName); hikariConfig.setJdbcUrl(jdbcUrl); hikariConfig.setUsername(username); // 设置初始令牌 String initialToken = IAMUtils.getIAMAuthToken(awsRegion, host, Integer.parseInt(port), username, awsRoleArn, SESSION_NAME); hikariConfig.setPassword(initialToken); // 其他连接池配置 hikariConfig.setIdleTimeout(CONNECTION_IDLE_TIMEOUT); hikariConfig.setMaxLifetime(CONNECTION_MAX_LIFETIME); hikariConfig.setKeepaliveTime(CONNECTION_KEEP_ALIVE_TIME); return new DynamicPasswordHikariDataSource(hikariConfig, awsRegion, host, Integer.parseInt(port), username, awsRoleArn, SESSION_NAME); }
内容的提问来源于stack exchange,提问作者Ebad
相关产品推荐
相关产品推荐

