我们有50多个微服务,每个连着3-5个数据库,150多个密码散落在application.yml里。直到有一天开发把生产密码提交到了GitHub,我才下定决心重构整套凭据管理方案。

起因:一次差点翻车的生产事故
去年底,我们团队有个 junior 开发在提交代码时,不小心把包含数据库连接串的 application-prod.yml 推到了 GitHub 仓库。虽然他5分钟后就发现了,赶紧删掉重新提交------但Git历史记录是删不掉的。
更让人后怕的是:安全团队的自动化扫描工具显示,在他推送后 18分钟内,就有一个来自海外的IP尝试用这些凭据连接我们的生产数据库。
最终处理结果:
- 紧急轮换了所有受影响的数据库密码(涉及12个系统)
- 逐个排查Git提交历史,清理敏感信息
- 花了3天时间才完成全部整改
- 该开发被通报批评
但复盘的时候我意识到一个更严重的问题:即使这次处理好了,同样的故事随时可能重演。因为我们的根本问题没有解决------150多个数据库密码,仍然明文躺在50多个服务的配置文件里。
一、我们原来的凭据管理有多"原始"?
先看看重构前的真实状态:
# 典型的 application-prod.yml
spring:
datasource:
url: jdbc:mysql://10.0.1.100:3306/order_db?useSSL=false
username: order_admin # 明文密码
password: Order@2024prod! # 明文密码!
driver-class-name: com.mysql.cj.jdbc.Driver
# 另一个服务的配置
spring:
datasource:
url: jdbc:mysql://10.0.1.100:3306/order_db?useSSL=false
username: order_admin # 同一个账号
password: Order@2024prod! # 同一个密码
driver-class-name: com.mysql.cj.jdbc.Driver
问题清单:
| 问题 | 具体表现 |
|---|---|
| 密码明文存储 | 所有环境(dev/test/prod)密码都以明文写在yml文件里 |
| 多服务共享密码 | 不同微服务连接同一个数据库时,直接复制粘贴同一个密码 |
| 密码从不轮换 | 这个 Order@2024prod! 从2024年建库到现在就没改过 |
| 权限无隔离 | 所有服务共用 order_admin 账号,没有按服务做最小权限 |
| 离职不回收 | 运维同学离职后,他本地还存着全套生产密码 |
| 应急没法改 | 真要改密码的话,需要逐个服务改配置、重启,至少停服1小时 |
这还不是最惨的。GitGuardian 2024年的报告显示,仅公开GitHub仓库中就发现了超过1000万条泄露的凭据,其中约12%是数据库连接密码。我们只是幸运没成为新闻而已。
二、动态凭据管理:核心概念3分钟搞懂
先解释一下什么是"动态凭据":
静态凭据(我们原来的方式):
密码 = "Order@2024prod!" ← 写死在配置文件里,永不过期
动态凭据(改造后的方式):
密码 = 平台临时生成 ← 有效期1小时,到期自动作废,每次都不一样
打个比方:静态凭据就像把公司大门钥匙配了50把,每人发一把,钥匙永远不过期 ;动态凭据就像每个人进门前刷指纹,临时生成一个一次性通行码,过了就失效。

核心工作流程
┌──────────┐ ① 请求临时密码 ┌──────────────┐ ③ 创建临时账号 ┌──────────┐
│ 微服务A │ ────────────────→ │ 凭据管理平台 │ ──────────────→ │ MySQL │
│ (Spring) │ │ │ ←────────────── │ │
│ │ ←─────────────── │ │ ④ 返回临时密码 │ │
└──────────┘ ② 返回临时密码 └──────────────┘ └──────────┘
+ 有效期1小时 │
⑤ 1小时后自动轮换
⑥ 旧密码作废,生成新密码
关键特性:
- 临时性:密码有效期可配(我们设的1小时),到期自动失效
- 唯一性:每个微服务实例拿到的是独立的临时密码
- 自动轮换:到期前平台自动完成密码更换,服务无需重启
- 零残留:配置文件里不再出现任何密码,代码泄露也不怕
- 可审计:每次密码申请、使用、销毁都有日志
三、技术选型:为什么没选HashiCorp Vault?
在动手之前,我们对比了三个方案:
| 维度 | HashiCorp Vault | 国产凭据管理平台 | 云厂商KMS |
|---|---|---|---|
| 部署方式 | 自建 | 本地/私有化 | 云托管 |
| 动态凭据 | ✅ 支持 | ✅ 支持 | ⚠️ 部分支持 |
| HSM集成 | ⚠️ 企业版付费 | ✅ 原生支持 | ✅ 云HSM |
| MySQL/PostgreSQL | ✅ 完善的Database Secret Engine | ✅ 支持主流数据库 | ⚠️ 有限支持 |
| 高可用 | ⚠️ 需要自建Consul集群 | ✅ 原生HA/灾备 | ✅ 云端托管 |
| 身份源集成 | ✅ LDAP/OIDC | ✅ LDAP/钉钉/企微/飞书 | ✅ 云IAM |
| 国密算法(SM2/SM3/SM4) | ❌ 不支持 | ✅ 原生支持 | ⚠️ 部分云支持 |
| 商密认证 | ❌ | ✅ | ❌ |
| 运维成本 | 高(需要专人维护Vault集群) | 中(厂商支持) | 低 |
最终选了国产凭据管理平台,理由很直接:
- 合规刚需:金融行业需要国密算法和商密认证,Vault不支持
- 运维门槛:Vault集群的运维复杂度高,我们没有专门的安全基础设施团队
- 身份源适配:需要对接钉钉审批流做密码紧急授权,国产平台原生支持
- 厂商支持:出了问题有人兜底,Vaul开源社区响应慢
四、落地实战:Spring Boot集成动态凭据
4.1 引入SDK依赖
xml
<!-- pom.xml -->
<dependency>
<groupId>com.secretsmanagement</groupId>
<artifactId>sms-spring-boot-starter</artifactId>
<version>3.2.0</version>
</dependency>
4.2 配置凭据客户端
yaml
# application.yml(注意:这个配置文件里不再有任何数据库密码!)
sms:
client:
server-url: https://sms.internal.company.com:8443
app-id: order-service-prod
app-token: ${SMS_APP_TOKEN} # 从环境变量读取,不是数据库密码
ssl:
trust-store: classpath:truststore.jks
trust-store-password: ${SMS_TRUST_PASSWORD}
secrets:
- name: order-db-credential
type: database
target: mysql://10.0.1.100:3306/order_db
role: order_service_readonly # 最小权限:只读
ttl: 1h # 临时密码有效期1小时
4.3 用注解替代硬编码密码
重构前的数据源配置:
java
// ❌ 重构前:密码写死在配置文件
@Configuration
public class DataSourceConfig {
@Bean
@Primary
public DataSource orderDataSource() {
HikariDataSource ds = new HikariDataSource();
ds.setJdbcUrl("jdbc:mysql://10.0.1.100:3306/order_db");
ds.setUsername("order_admin");
ds.setPassword("Order@2024prod!"); // 明文密码!
ds.setMaximumPoolSize(20);
return ds;
}
}
重构后:
java
// ✅ 重构后:密码由凭据平台动态注入
@Configuration
public class DataSourceConfig {
@Bean
@Primary
@SmsDynamicCredential(name = "order-db-credential")
public DataSource orderDataSource() {
HikariDataSource ds = new HikariDataSource();
ds.setJdbcUrl("jdbc:mysql://10.0.1.100:3306/order_db");
// username 和 password 由 @SmsDynamicCredential 自动注入
ds.setMaximumPoolSize(20);
return ds;
}
}
核心变化:
- 不再需要
setUsername()和setPassword() @SmsDynamicCredential注解会在应用启动时自动向凭据平台请求临时密码- 密码到期前SDK自动完成轮换,连接池中的旧密码会被平滑替换,无需重启
4.4 处理连接池的密码轮换问题
这是落地过程中最容易被忽略的技术细节:
HikariCP连接池中的连接是用旧密码建立的,密码轮换后这些连接会认证失败。
解决方案是在配置中启用SDK的自动刷新机制:
yaml
sms:
rotation:
enabled: true
strategy: graceful # 平滑轮换策略
pre-rotate-seconds: 300 # 提前5分钟获取新密码
max-retry: 3 # 轮换失败重试次数
evict-connections: true # 轮换后逐个淘汰旧连接
min-idle-connections: 5 # 淘汰过程中保持最小空闲连接
SDK会在密码到期前5分钟提前获取新密码,然后逐个淘汰使用旧密码的连接。新请求会使用新密码建立连接,整个过程对业务无感知。
4.5 验证动态凭据是否生效
写一个简单的健康检查接口来验证:
java
@RestController
@RequestMapping("/actuator")
public class CredentialHealthController {
@Autowired
private SmsCredentialService credentialService;
@GetMapping("/credential-status")
public Map<String, Object> getCredentialStatus() {
SmsCredentialInfo info = credentialService.getCredentialInfo("order-db-credential");
return Map.of(
"secretName", info.getName(),
"username", info.getUsername(), // 临时用户名(如 sms_tmp_182736)
"issuedAt", info.getIssuedAt().toString(),
"expiresAt", info.getExpiresAt().toString(),
"remainingSeconds", info.getRemainingSeconds(),
"status", info.isExpired() ? "EXPIRED" : "ACTIVE"
);
}
}
返回示例:
json
{
"secretName": "order-db-credential",
"username": "sms_tmp_18273645",
"issuedAt": "2026-05-08T10:00:00",
"expiresAt": "2026-05-08T11:00:00",
"remainingSeconds": 2847,
"status": "ACTIVE"
}
每次调用这个接口,如果距离上次调用超过了配置的TTL,你会看到 username 和 remainingSeconds 发生变化------说明密码确实在动态轮换。
五、50个微服务的分批改造策略
我们没有一口气改完所有服务,而是分了4批:
第一批:核心交易系统(3个服务)
选这三个的原因:访问量最高、安全风险最大、改造效果最明显。
改造清单:
1. order-service(订单服务)--- 生产数据库连接
2. payment-service(支付服务)--- 支付数据库连接
3. inventory-service(库存服务)--- 库存数据库连接
踩的坑:
- 支付服务用了
ShardingSphere做分库分表,SDK需要额外配置 ShardingSphere 的数据源适配器 - 库存服务有读写分离,需要在凭据平台配置两个凭据条目(读凭据和写凭据),对应不同的数据库权限
改造后效果:
- 3个核心服务的数据库密码每1小时自动轮换
- 即使某个服务的密码泄露,最多1小时后自动失效
- 安全审计报告可以一键导出
第二批:运营后台系统(8个服务)
- CRM、工单、报表等服务
- 这些服务特点是:部分使用遗留的JDBC直接连接方式,没有用ORM框架
- 对这种老系统,采用了 Sidecar代理模式:在服务旁边部署一个轻量级Agent,拦截JDBC连接请求,自动注入临时密码
第三批:数据分析系统(12个服务)
- 数仓、ETL、BI报表等服务
- 特点是:需要长期运行的数据同步任务(可能跑几个小时)
- 解决方案:将TTL调长到24小时,并在任务启动时"续租"凭据
第四批:所有剩余服务(27个)
- 有了前三批的经验和SDK模板,这批基本就是"复制粘贴 + 修改配置"
- 每个服务平均改造时间缩短到 30分钟
六、生产环境的几个关键配置
6.1 数据库权限最小化
动态凭据不只是"换了个密码",更重要的是权限隔离:
sql
-- 传统方式:一个管理员账号走天下
GRANT ALL PRIVILEGES ON order_db.* TO 'order_admin'@'%';
-- 动态凭据方式:每个服务一个专属账号,最小权限
-- 订单服务:只需要读写 orders 表
CREATE USER 'sms_tmp_order_svc'@'%' IDENTIFIED BY '动态生成的密码';
GRANT SELECT, INSERT, UPDATE ON order_db.orders TO 'sms_tmp_order_svc'@'%';
-- 报表服务:只需要只读权限
CREATE USER 'sms_tmp_report_svc'@'%' IDENTIFIED BY '动态生成的密码';
GRANT SELECT ON order_db.* TO 'sms_tmp_report_svc'@'%';
凭据平台会自动完成账号的创建和密码设置,DBA不需要手动操作。
6.2 密码轮换的灰度策略
对核心交易系统,我们用了灰度轮换:
第一轮:先替换 1 个实例的密码,观察 30 分钟
→ 如果没有连接异常,进入下一步
第二轮:替换 25% 实例的密码,观察 1 小时
→ 监控慢查询和连接超时
第三轮:全量替换
→ 确认所有实例都在使用新密码
6.3 异常告警配置
告警规则:
1. 凭据获取失败 → 立即告警(P0级别)--- 可能是凭据平台故障
2. 凭据频繁申请(10次/分钟)→ 告警(P1级别)--- 可能是攻击或异常行为
3. 凭据即将过期未续租 → 告警(P2级别)--- 可能是服务异常停止
4. 非授权来源IP申请凭据 → 立即告警(P0级别)--- 可能是入侵
七、改造前后的效果对比
| 维度 | 改造前 | 改造后 |
|---|---|---|
| 密码存储 | 明文写在yml文件中 | 凭据平台加密存储,HSM保护 |
| 密码有效期 | 永不过期 | 1小时自动轮换 |
| 配置文件中是否有密码 | 是(150+个文件) | 否(0个) |
| 权限隔离 | 所有服务共用1个管理员账号 | 每个服务独立账号,最小权限 |
| 密码泄露影响 | 泄露后长期有效,需人工逐个改 | 最多1小时后自动失效 |
| 离职员工风险 | 本地密码仍有效 | 凭据平台统一回收,即时生效 |
| 合规审计 | 手动查日志,耗时耗力 | 一键导出凭据使用报告 |
| 应急响应 | 改密码需要停服1小时+ | 吊销凭据秒级生效 |
写在最后
说实话,动态凭据管理这个方案并不新鲜------HashiCorp Vault 早在2015年就开源了。但直到我们自己踩了坑,才真正意识到:它不是一个"锦上添花"的安全工具,而是一个"迟早要做"的基础设施。
如果你也面临类似的问题------50+服务的密码散落一地、改个密码要全量重启、出了安全事件不知道该改哪些密码------建议尽早把凭据管理这件事提上日程。
从哪里开始:
- 先盘点你的系统里有多少"明文密码"(结果可能会让你吃惊)
- 选一个最核心的系统做试点,验证SDK集成方案
- 有了经验后再批量推广
关于选型:如果你的企业有国密合规要求(金融、政务、等保三级以上),国产凭据管理平台在国密算法、商密认证、身份源适配方面有天然优势。如果纯技术场景且没有合规限制,Vault仍然是生态最丰富的选择。