手把手:Spring Boot接入凭据管理服务完整代码 + 5个踩坑记录 前言你的数据库密码现在在哪如果你的 Spring Boot 项目的application.yml里有这样的配置spring:datasource:username:rootpassword:DbProd2024!那就需要认真看这篇文章了。静态密码写在配置文件里有多危险代码仓库里的历史提交记录一旦泄露所有密码全部曝光运维、开发共享同一份配置知道密码的人过多密码长期不换一旦泄露极难感知容器镜像中包含明文配置Docker Hub 一旦公开即泄露GitHub 的 Secret Scanning 每年发现超过1000万个泄露的 API Key 和密码。其中 Spring Boot 配置文件泄露占比超过 15%。本文手把手演示如何把这些明文密码迁移到凭据管理服务让应用启动时动态拉取密码定期自动轮转整个过程应用代码零改造。一、核心概念凭据管理服务做什么传统方式危险 application.yml → 明文密码 → 直接连数据库 凭据管理方式安全 application.yml → 凭据ID → 凭据管理服务 → 动态密码 → 连数据库 ↑ 密码由这里统一管理 定期自动轮转 访问全程审计 密码永远不落盘核心价值密码不落盘配置文件里只有凭据ID密码只在内存中短暂存在自动轮转凭据管理服务定期换密码应用无感知访问审计每次拉取凭据都有日志随时知道谁在什么时候用了哪个密码最小权限不同应用只能拉取自己的凭据互相隔离二、接入方案三种集成深度选择方案改造量适用场景方案AStarter自动注入最小只改yml新项目或标准Spring Boot项目方案BPropertySource扩展中等需实现一个类需要精细控制、兼容已有框架方案CBean初始化时拉取最大但最灵活复杂多数据源、遗留系统改造本文主要演示方案A其余方案附代码片段供参考。三、方案AStarter 接入推荐最简单3.1 Maven 依赖dependencygroupIdcn.andang/groupIdartifactIdsms-spring-boot-starter/artifactIdversion2.3.1/version/dependency3.2 application.yml 最小配置# # 凭据管理服务连接配置# sms:server-url:https://sms.internal.company.com:8443app-id:your-app-id-here# 应用标识在凭据管理平台注册时获取app-secret:${SMS_APP_SECRET}# 应用密钥从环境变量获取本身不写死tls-verify:true# 生产环境必须开启cache-ttl:300# 凭据缓存时间秒0不缓存# # 数据库配置 - 密码字段改为凭据引用# spring:datasource:driver-class-name:com.mysql.cj.jdbc.Driverurl:jdbc:mysql://db.internal.company.com:3306/prod_db?useSSLtrueusername:appuserpassword:${sms:credential/prod-db-password}# ← 凭据引用格式redis:host:redis.internal.company.comport:6379password:${sms:credential/prod-redis-password}# ← Redis密码也可以用# # 第三方API Key - 同样可以用凭据管理# third-party:payment-api-key:${sms:credential/payment-gateway-key}sms-access-key:${sms:credential/sms-service-key}就这样其他什么都不用改。Starter 会自动在 Spring 启动时连接凭据管理服务解析所有${sms:credential/xxx}格式的占位符从服务端拉取实际密码注入到对应的 Bean 中3.3 验证接入成功SpringBootTestclassCredentialIntegrationTest{AutowiredprivateDataSourcedataSource;TestvoidtestDatabaseConnection()throwsException{// 验证数据库连接是否正常意味着凭据拉取成功try(ConnectionconndataSource.getConnection()){assertTrue(conn.isValid(5));System.out.println(数据库连接成功 ✓ 凭据拉取正常);}}}四、3个实战代码场景场景1多数据源配置主库从库ConfigurationpublicclassMultiDataSourceConfig{// 主库从凭据管理服务获取密码Bean(primaryDataSource)ConfigurationProperties(prefixspring.datasource.primary)publicDataSourceprimaryDataSource(){returnDataSourceBuilder.create().build();}// 从库从凭据管理服务获取密码Bean(replicaDataSource)ConfigurationProperties(prefixspring.datasource.replica)publicDataSourcereplicaDataSource(){returnDataSourceBuilder.create().build();}}spring:datasource:primary:url:jdbc:mysql://master-db:3306/produsername:app_masterpassword:${sms:credential/master-db-password}# 主库凭据replica:url:jdbc:mysql://slave-db:3306/produsername:app_replicapassword:${sms:credential/replica-db-password}# 从库凭据单独管理场景2凭据热更新轮转后应用不重启这是凭据管理最核心的价值之一密码轮转后应用无需重启。ComponentpublicclassCredentialRefreshListener{AutowiredprivateSmsCredentialManagercredentialManager;AutowiredprivateHikariDataSourcedataSource;/** * 监听凭据变更事件凭据管理服务推送 * 收到通知后关闭旧连接池用新密码重建 */EventListenerpublicvoidonCredentialRotated(CredentialRotatedEventevent){if(prod-db-password.equals(event.getCredentialId())){log.info(收到数据库密码轮转通知开始热更新连接池...);// 从凭据管理服务拉取新密码StringnewPasswordcredentialManager.getCredential(prod-db-password);// 优雅关闭旧连接池等待现有连接执行完成dataSource.getHikariPoolMXBean().softEvictConnections();// 更新密码配置dataSource.setPassword(newPassword);log.info(连接池密码热更新完成无停机时间 ✓);}}}场景3动态 API Key 获取第三方服务集成ServicepublicclassPaymentService{AutowiredprivateSmsCredentialManagercredentialManager;publicPaymentResultprocessPayment(PaymentRequestrequest){// 每次调用前动态获取最新API Key自动走缓存不频繁请求凭据服务StringapiKeycredentialManager.getCredential(payment-gateway-key);// 使用动态获取的API Key调用支付接口PaymentClientclientPaymentClient.builder().apiKey(apiKey).build();returnclient.charge(request);}}五、5个踩坑记录坑1证书校验失败应用启动报错报错javax.net.ssl.SSLHandshakeException: PKIX path building failed: unable to find valid certification path to requested target原因凭据管理服务使用了内部CA签发的证书JVM的默认信任库里没有这个CA根证书。解决把内部CA根证书导入JVM信任库或者在Starter配置里指定信任的CAsms:tls-trust-store:/opt/certs/internal-ca.jkstls-trust-store-password:changeit或者全局导入推荐# 把内部CA证书导入JVM信任库keytool-import-trustcacerts\-keystore$JAVA_HOME/lib/security/cacerts\-storepasschangeit\-aliasinternal-ca\-file/path/to/internal-ca.crt坑2超时导致应用启动失败报错SmsConnectionException: Connect to sms.internal.company.com:8443 failed after 5000ms BeanCreationException: Error creating bean with name dataSource原因默认连接超时5秒在网络较差或凭据服务刚启动时可能超时。解决sms:connect-timeout:15000# 连接超时15秒read-timeout:10000# 读取超时10秒retry-times:3# 失败重试3次retry-interval:2000# 重试间隔2秒同时建议凭据管理服务做高可用部署避免单点故障导致所有应用无法启动sms:server-urls:# 配置多个服务地址-https://sms-node1.internal.company.com:8443-https://sms-node2.internal.company.com:8443failover:true# 自动故障转移坑3测试环境 Mock 凭据问题单元测试不应该真的去连接凭据管理服务速度慢、有依赖。解决Starter 支持 Mock 模式// 测试类SpringBootTestTestPropertySource(properties{sms.mock-modetrue// 开启Mock不真实请求凭据服务})classServiceTest{MockBeanprivateSmsCredentialManagercredentialManager;BeforeEachvoidsetUp(){// Mock 凭据返回值when(credentialManager.getCredential(prod-db-password)).thenReturn(test-password-for-unit-test);}TestvoidtestBusinessLogic(){// 测试业务逻辑不依赖真实凭据}}或者直接在application-test.yml中配置固定值只用于本地开发/CI环境# application-test.ymlspring:datasource:password:test_db_password_only_for_ci# 测试环境不用凭据服务坑4容器化部署时SMS_APP_SECRET环境变量未注入问题Docker 容器内${SMS_APP_SECRET}解析为空字符串导致认证失败。原因docker run时没有传入环境变量或 Kubernetes Secret 没有正确挂载。解决Kubernetes方式# k8s-deployment.yamlapiVersion:apps/v1kind:Deploymentspec:template:spec:containers:-name:appenv:-name:SMS_APP_SECRETvalueFrom:secretKeyRef:name:sms-credentials# K8s Secret名称key:app-secret# Secret中的key# 创建 K8s Secretkubectl create secret generic sms-credentials\--from-literalapp-secretyour-app-secret-here坑5MyBatis 分页插件与连接池密码更新冲突问题密码热更新后MyBatis PageHelper 插件持有的旧连接导致后续查询报认证错误。原因PageHelper 持有了连接池的引用softEvictConnections()无法驱逐被 PageHelper 占用的连接。解决在热更新时同时触发 PageHelper 的连接释放EventListenerpublicvoidonCredentialRotated(CredentialRotatedEventevent){if(prod-db-password.equals(event.getCredentialId())){// 先清空 PageHelper 的连接缓存PageHelper.clearPage();// 再执行连接池热更新dataSource.getHikariPoolMXBean().softEvictConnections();dataSource.setPassword(credentialManager.getCredential(prod-db-password));log.info(连接池和分页插件同步更新完成 ✓);}}六、单元测试完整写法ExtendWith(MockitoExtension.class)classCredentialServiceTest{MockprivateSmsCredentialManagercredentialManager;InjectMocksprivateDatabaseConnectionServiceconnectionService;TestDisplayName(正常场景从凭据服务获取密码成功)voidtestGetCredentialSuccess(){// Givenwhen(credentialManager.getCredential(prod-db-password)).thenReturn(dynamic-password-xyz);// WhenStringpasswordconnectionService.getDatabasePassword();// ThenassertThat(password).isEqualTo(dynamic-password-xyz);verify(credentialManager,times(1)).getCredential(prod-db-password);}TestDisplayName(异常场景凭据服务不可用时的降级处理)voidtestCredentialServiceUnavailable(){// Given模拟凭据服务超时when(credentialManager.getCredential(anyString())).thenThrow(newSmsConnectionException(Connection timeout));// When Then应该抛出业务异常而不是直接崩溃assertThatThrownBy(()-connectionService.getDatabasePassword()).isInstanceOf(CredentialUnavailableException.class).hasMessageContaining(无法获取数据库密码);}TestDisplayName(缓存场景60秒内重复调用只拉取一次)voidtestCredentialCaching(){// Givenwhen(credentialManager.getCredential(prod-db-password)).thenReturn(cached-password);// When连续调用5次for(inti0;i5;i){connectionService.getDatabasePassword();}// Then实际只请求了1次其余走缓存verify(credentialManager,times(1)).getCredential(prod-db-password);}}总结把 Spring Boot 的明文密码迁移到凭据管理服务核心步骤只有三步加依赖sms-spring-boot-starter改配置密码字段从明文改为${sms:credential/xxx}配环境变量SMS_APP_SECRET通过环境变量注入整个过程业务代码零改动存量项目也能快速接入。最重要的5个坑——证书信任、超时重试、测试Mock、容器变量注入、热更新冲突——按本文方案处理可以节省大量排查时间。如果你的项目还在用明文密码从今天开始迁移一台服务器的改造成本只有半小时。 话题讨论你们项目里的数据库密码现在是明文存在配置文件里还是用了凭据管理方案有没有遇到过密码泄露的经历欢迎评论区聊聊。