我们有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集群) 中(厂商支持)

最终选了国产凭据管理平台,理由很直接:

  1. 合规刚需:金融行业需要国密算法和商密认证,Vault不支持
  2. 运维门槛:Vault集群的运维复杂度高,我们没有专门的安全基础设施团队
  3. 身份源适配:需要对接钉钉审批流做密码紧急授权,国产平台原生支持
  4. 厂商支持:出了问题有人兜底,Vaul开源社区响应慢

四、落地实战:Spring Boot集成动态凭据

4.1 引入SDK依赖

<!-- pom.xml -->
<dependency>
    <groupId>com.secretsmanagement</groupId>
    <artifactId>sms-spring-boot-starter</artifactId>
    <version>3.2.0</version>
</dependency>

4.2 配置凭据客户端

# 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 用注解替代硬编码密码

重构前的数据源配置:

// ❌ 重构前:密码写死在配置文件
@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;
    }
}

重构后:

// ✅ 重构后:密码由凭据平台动态注入
@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的自动刷新机制:

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 验证动态凭据是否生效

写一个简单的健康检查接口来验证:

@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"
        );
    }
}

返回示例:

{
  "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,你会看到 usernameremainingSeconds 发生变化——说明密码确实在动态轮换。


五、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 数据库权限最小化

动态凭据不只是"换了个密码",更重要的是权限隔离

-- 传统方式:一个管理员账号走天下
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+服务的密码散落一地、改个密码要全量重启、出了安全事件不知道该改哪些密码——建议尽早把凭据管理这件事提上日程。

从哪里开始

  1. 先盘点你的系统里有多少"明文密码"(结果可能会让你吃惊)
  2. 选一个最核心的系统做试点,验证SDK集成方案
  3. 有了经验后再批量推广

关于选型:如果你的企业有国密合规要求(金融、政务、等保三级以上),国产凭据管理平台在国密算法、商密认证、身份源适配方面有天然优势。如果纯技术场景且没有合规限制,Vault仍然是生态最丰富的选择。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐