你有没有在项目的配置文件里见过这样一行:password: 123456?我见过太多次。多数团队把数据库密码晒在配置仓库的阳光下,等到某天发现 Git 历史里出现一串不该出现的连接串,才开始后悔。今天聊的解决办法不复杂:使用 Druid 连接池自带的 ConfigTools 能力,对数据库密码做非对称加密,把明文密码从配置文件里彻底请出去。这个方案不需要引入额外框架,适合已经在用 Druid 连接池、尤其是 Spring Boot 项目,并且每天要跟数据库配置打交道的后端同学。读完这篇,你可以独立完成密钥生成、配置改造、启动验证和后续维护,也能顺手把团队配置管理里的安全漏洞补上。
1. 为什么要给数据库密码加密,以及 Druid 方案的取舍
1.1 明文密码躺在配置里的真实风险
在聊技术实现之前,先说一个场景。很多项目的application.yml是直接放在 Git 仓库里的,甚至还有把多个环境的连接信息写在一个文件里的情况。只要仓库权限管理稍微松一点,任何能读取代码的人都能拿到生产数据库的账号密码。这不是假设,我见过不止一次因为配置仓库泄露,导致数据库被频繁登录的事件。
明文密码的风险不只是仓库泄露,还有更隐蔽的三条渠道:
- 日志与排障过程:应用启动失败时,有人为了排查问题,会把配置文件完整复制到聊天工具里,密码就在里面传播。
- 员工交接与离职:数据库连接配置通常存在于老员工笔记、本地历史命令、甚至 Docker 启动脚本里,换人之后不知道密码被谁看过。
- 审计合规压力:现在很多业务要过安全审计,数据库账号密码明文存储在配置库这一项基本是必查问题。
密码加密并不是为了防住一个绝对技术高超的攻击者,而是把“拿到配置文件就能连数据库”这件事的难度提高一个量级。哪怕攻击者拿到了密文和公钥,也还原不出真实密码。
1.2 常见加密方案对比
现在做数据库密码加密,可选的路子其实不少,但各有代价:
| 方案 | 加解密原理 | 优点 | 缺点 |
|---|---|---|---|
| 环境变量存放 | 不加密,只把明文移出配置文件 | 简单、灵活 | 环境变量泄露等于密码泄露;没有解决根本保护问题 |
| 配置中心动态管理 | 密文或明文由配置中心下发 | 支持动态刷新,集中管理 | 配置中心本身变成单点信任;仍需要处理加解密流程 |
| 自定义加解密工具类 | 自己写一套脱敏/还原逻辑 | 可控性强 | 需要自己维护密钥;容易出现“固定密钥写死在代码里”的笑话 |
| 专业加密库方案 | 启动时用密钥库解密配置属性 | 成熟、功能多 | 引入额外依赖,配置复杂度高,团队需要一定学习成本 |
| Druid ConfigTools | 非对称加密,运行期自动解密 | 零依赖增量,Druid 自带;只需配公钥和密文 | 仅适用于 Druid 连接池场景 |
如果项目里已经用了 Druid 作为连接池,那选 Druid ConfigTools 是性价比最高的。它不需要另起炉灶,不增加新的 jar 包(ConfigTools 就在 druid 主包里),不要求你去理解复杂的密钥库规范,生成密钥和密文也就是几条命令的事情。
1.3 Druid 的加密解密链路设计
Druid 的这套机制,底层是非对称加密。用生活类比来说:私钥就像一枚刻章,密文是盖过章的凭证,公钥则是一台专门验章的机器。只有拿着刻章的人能制造凭证,但任何人只要拿到验章机,都能确认凭证内容是否真实有效。
流程拆开是这样的:
- 开发或运维人员使用 ConfigTools 生成密钥对:一个私钥,一个公钥。
- 私钥用来加密数据库明文密码,产出密文。
- 配置文件中不再写明文,而是写这段密文。
- Druid 连接池启动时,ConfigFilter 过滤器读取
config.decrypt=true和config.decrypt.key(也就是公钥),用公钥把密文还原成明文密码。 - 还原后的明文密码被用来建立真实数据库连接,之后的连接池行为跟普通配置完全一致。
关键点在于:应用运行期只需要公钥,不需要私钥。公钥本身是公开信息,就算被人拿走,也无法反向推出数据库密码。私钥只在生成密文的环节出现一次,可以离线保存,从源头上减少了泄露面。
2. 实操:五分钟跑通 Druid 数据库密码加密
2.1 引入依赖时的版本选择
如果项目是 Spring Boot,通常直接使用集成好的 starter 即可。在pom.xml里引入依赖:
<dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.2.20</version> </dependency>版本这里我多说一句:不要拿一个太老的 1.1.x 版本硬套在 Spring Boot 2.4 以上的项目里,配置绑定机制换过好几轮,老版本会出现“明明配了却不生效”的诡异问题。建议直接使用 1.2.x 较新的版本,并且让 druid 主版本在项目内保持一致,避免同时存在多个 druid jar。
如果项目没有使用 Spring Boot,也不想引入 starter,只引入druid主包也可以:
<dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.20</version> </dependency>2.2 使用 ConfigTools 生成密钥对和密文
Druid 的 ConfigTools 提供了命令行模式,直接用 jar 包就能生成,不需要写代码。先确认本机 Maven 仓库里有对应版本的 druid jar,然后执行:
java -cp ~/.m2/repository/com/alibaba/druid/1.2.20/druid-1.2.20.jar com.alibaba.druid.filter.config.ConfigTools "你的数据库密码"注意:如果你是在 Windows 上的 PowerShell 或者命令行里执行,路径分隔符要换成\,且密码中的特殊字符最好用英文双引号包住,防止 shell 截断。
执行完成后,终端会输出三行关键内容:
privateKey: MIIEvQIBADANBg...(私钥) publicKey: MIIBIjANBgk...(公钥) password: B7nQa0SN5elq...(密文)这三行值各自的用途:
privateKey:私钥。只在生成密文的时候使用,生成完就要妥善保管,绝对不能进配置文件、不能进 Git。publicKey:公钥。填到应用配置里,告诉 Druid 用什么来解密。password:加密后的密文。替换掉原来application.yml里的明文密码。
如果你不方便执行命令行,也可以临时写一个 main 方法,调用 ConfigTools 生成:
public class PasswordGenerator { public static void main(String[] args) throws Exception { String[] pair = com.alibaba.druid.filter.config.ConfigTools.genKeyPair(); String privateKey = pair[0]; String publicKey = pair[1]; String encryptedPwd = com.alibaba.druid.filter.config.ConfigTools.encrypt(privateKey, "你的数据库密码"); System.out.println("privateKey: " + privateKey); System.out.println("publicKey: " + publicKey); System.out.println("password: " + encryptedPwd); } }跑完之后记得把临时类删掉,不要留在项目源码里。命令行方式生成时,还建议确认终端历史记录不会把带密码的命令同步到团队共享环境。
2.3 修改配置文件:三个关键项一个都不能少
拿到密文和公钥之后,修改application.yml。下面是一份完整的配置示例,假设数据源配置在spring.datasource.druid前缀下(这种方式在较新的 druid starter 里最稳妥):
spring: datasource: druid: url: jdbc:mysql://localhost:3306/your_db?useUnicode=true&characterEncoding=utf8 username: your_db_user password: "B7nQa0SN5elq..." driver-class-name: com.mysql.cj.jdbc.Driver filter: config: enabled: true connection-properties: "config.decrypt=true;config.decrypt.key=MIIBIjANBg...公钥内容..."这里有三个关键配置项,缺一不可:
password:从明文替换成加密后的密文。filter.config.enabled: true:告诉 Druid 启用 ConfigFilter 过滤器,让它有机会对密码进行解密。connection-properties:把解密开关和公钥以连接属性的方式传给 ConfigFilter。config.decrypt=true表示需要解密,config.decrypt.key就是公钥本体。
如果你用的是spring.datasource.url这种不带druid前缀的写法,也可以在 Spring Boot 的自动装配场景下生效,但需要注意不同版本的 starter 读取前缀的差别。我的建议是干脆统一写在spring.datasource.druid下,一次性把 url、username、password 都交给 Druid 管理,减少阴间问题。
密文和公钥都是很长的一串 Base64 字符,YAML 中建议用双引号包起来。虽然 Base64 里没有冒号、井号这类高危险字符,但双引号能避免某些解析器在长字符串上做奇怪截断。
2.4 启动验证:怎么确认真的生效了
配置改完,启动应用,正常会在启动日志里看到 Druid 连接池初始化的信息。如果直接连数据库成功,且接口能正常查询数据,说明解密链路是通的。
但我还建议你做一次主动验证:把password这一项的值随便改掉一位,然后重新启动。这时候会有两种情况:
- 如果配置链路生效,Druid 会用公钥去解这段错误密文,大概率直接抛出解密失败的异常。
- 如果看到的是连接数据库时
Access denied for user ... (using password: YES),说明密码已经成功解密,只是解出来的值不对,这时候要去检查“密文到底是用哪把私钥加密的”。
这两种报错指向的问题完全不同,提前分辨清楚,后续排障会快很多。
2.5 多数据源与原生配置姿势
多数据源场景稍微复杂一点,但原理是一样的:每个数据源都需要有自己的filter.config.enabled和connection-properties。如果你在 Spring Boot 里用@ConfigurationProperties扩展了多个数据源,每一组 Druid 配置都要把解密参数补齐。公钥可以共用一份,因为多套环境的公钥并不冲突,但建议按环境隔离,下一章会细说。
如果不使用 starter,而是手动创建DruidDataSource,配置方式稍有不同。核心是给 dataSource 设置connectionProperties,并显式指定过滤器:
DruidDataSource dataSource = new DruidDataSource(); dataSource.setUrl("jdbc:mysql://localhost:3306/your_db"); dataSource.setUsername("your_db_user"); dataSource.setPassword("B7nQa0SN5elq..."); dataSource.setConnectionProperties("config.decrypt=true;config.decrypt.key=MIIBIjANBg..."); dataSource.setFilters("config,stat,wall");注意这里的setFilters("config,stat,wall"),如果只写"stat",ConfigFilter 不会启用,密文会被当成真正的密码去连接数据库,必然失败。
3. 核心参数解析与密钥管理经验
3.1 config.decrypt 和 config.decrypt.key 的作用边界
很多人配完之后只记住了“照抄就行”,一旦报错就懵了。所以我把这两个参数的作用说得再透一点。
config.decrypt=true是给 ConfigFilter 的一个信号,告诉它:当前数据源连接属性里藏着需要解密的字段。config.decrypt.key则是解密时要用到的公钥。两者是配合关系,只写config.decrypt=true不写 key,过滤器不知道用哪把“钥匙”去解;只写 key 不写config.decrypt=true,过滤器默认不干活。
这里有一个很容易踩到的边界问题:connection-properties里的参数最终会成为 Druid 建立连接前的属性集合,ConfigFilter 在连接创建之前拦截并处理。如果项目里存在多个过滤器,且自定义过滤器把连接属性提前消费掉了,ConfigFilter 可能拿不到完整信息。最常见的情况就是别人新增了一个自定义Filter,把connectionProperties里的参数清空或者覆盖了,导致解密配置不生效。
3.2 为什么是非对称而不是“写死一个密钥做 AES”
不少人看到这套机制会问:直接用同一个密钥加密解密,不是更简单吗?确实,对称加密在实现上更直观,但有一个致命问题:如果用来加密的密钥和生产环境用来解密的密钥是同一个,那这个密钥就必然要出现在应用侧配置或代码里,否则无法运行。一旦攻击者拿到配置和密钥,加密形同虚设。
非对称加密的价值在于“加密用的私钥”和“解密用的公钥”可以分开放。公钥随便给人看,私钥锁在保险柜里。配置文件、Git 仓库、应用服务器上只有密文和公钥,就算全部泄露,也拿不到数据库明文密码。这也是我为什么不推荐自己写一套 AES 工具类来做这件事的原因——密钥管理很容易失控,一旦密钥被写进代码,还不如不加密。
3.3 公钥、私钥、密文的保管边界
这是整个加密方案里最关键的运维红线。我遇到过团队把生成的密钥对直接写进 README 的,也有把私钥放进 Git 仓库的,这种操作等于白加了一层壳。
| 内容 | 能否提交代码仓库 | 原因 |
|---|---|---|
密文password | 可以 | 密文依赖私钥才能还原,仓库泄露密度相对低 |
公钥publicKey | 可以 | 公钥公开也无妨,无法通过公钥反推私钥 |
私钥privateKey | 绝对不能 | 拿到私钥的人可以自己加密任意密码,再配合公钥还原,等于完全绕过保护 |
| 数据库明文密码 | 绝对不能 | 明文一旦进仓库,之前的加密改造全部白费 |
私钥建议保存在团队的密码管理工具里,或者由负责数据库安全的管理员离线保管。生成密文的过程最好在本地开发机或者专用运维机上完成,不要在 CI 构建机上生成,避免密钥流经构建日志。
3.4 多环境密钥规划与轮换方案
开发环境、测试环境、生产环境如果共用同一个密钥对,那一旦开发环境私钥泄露,生产库密码也跟着暴露。我见过有团队为了图省事,把所有环境的数据库密码都用同一把私钥生成。这种做法风险很大,等于把生产安全押在开发环境的管理水平上。
建议每套独立环境生成独立的密钥对,至少要做到生产环境单独一套。配合 Spring Boot 的多 profile 配置,可以这样设计:
# application-prod.yml spring: datasource: druid: connection-properties: "config.decrypt=true;config.decrypt.key=${DB_PUBLIC_KEY}"公钥通过环境变量DB_PUBLIC_KEY注入,不写在代码仓库中。代码仓库里只保留密文,生产部署平台通过密钥管理功能动态注入公钥,这样即使仓库完全公开,生产环境仍然无法被直接连接。
密钥轮换的情况通常是数据库密码变更或私钥疑似泄露。轮换步骤建议按这个顺序来:
- 生成新的密钥对。
- 用新私钥加密新的数据库密码(或不变的密码)。
- 把新密文写入配置文件,新公钥更新到配置中心或环境变量。
- 先让一个节点滚动更新并验证,确认连接正常后再全量发布。
- 确认稳定后,销毁旧的私钥和旧的公钥配置。
4. 常见问题与排查技巧实录
4.1 高频报错速查表
下面这张表是我在团队项目和社区交流里整理出来的高频问题,按“现象 → 原因 → 解法”整理,排障时直接对照可以省很多时间。
| 报错现象 | 可能原因 | 排查方向和解决 |
|---|---|---|
启动报decrypt failed | 公钥与密文不匹配;公钥字符串被空格或换行污染 | 确认密文是由同一把私钥生成的;检查config.decrypt.key是否完整复制,去掉首尾空格 |
启动报Access denied for user ... | 解密流程正常,但解密后的密码不对 | 检查原始明文中途是否被 shell 截断;重新用正确密码生成密文 |
| 启动正常但查询时报连接失败 | connection-properties没被 ConfigFilter 读取到;自定义 Filter 覆盖了属性 | 显式在配置中启用filter.config.enabled或setFilters("config,...");检查是否存在自定义过滤器篡改属性 |
| 配置了密文但连接时仍用明文 | 配置文件前缀写错,Spring Boot 根本没有把配置绑定到 DruidDataSource | 统一使用spring.datasource.druid.*前缀;查看启动日志确认数据源属性 |
| yml 启动就报格式错误 | 密文或公钥没有加引号,YAML 解析出现意外 | 将密文和公钥用英文双引号包裹 |
| 本地环境正常,生产环境报解密失败 | 生产公钥来自另一套密钥;环境变量未注入 | 分别生成各环境密钥;检查生产环境DB_PUBLIC_KEY是否为空或错误 |
| 使用高版本 JDK 报 RSA 解密相关异常 | 部分旧版本 druid 与 JDK 安全策略存在兼容问题 | 升级 druid 版本,或更换 JDK 小版本 |
4.2 三个真实踩坑案例复盘
第一个坑:私钥跟着 Git 历史走。某次给团队做安全整改时发现,生成密钥对的代码被提交到了仓库,私钥以字符串形式躺在工具类里。虽然当时没有真实攻击发生,但整个加密体系已经失效。整改方案是重新生成密钥对、更新所有密文配置,然后把旧提交从仓库历史里清理掉,并在团队规范里规定私钥严禁入库。这件事告诉我们:技术方案做得再周全,密钥管理流程跟不上等于零。
第二个坑:配置中心的公钥占位符被解析成空字符串。某个项目把公钥放到配置中心管理,配置项写的是${DB_PUBLIC_KEY},但部署平台没有配置这个变量,导致启动时连接属性变成config.decrypt=true;config.decrypt.key=,ConfigFilter 拿到空公钥,直接抛解密失败。排查了半天,最后发现是环境变量拼写不一致。解决办法很简单,在应用启动脚本里加了一个显式校验:如果公钥为空,直接启动失败并输出清晰提示,避免以半明半密的状态跑起来。
第三个坑:生成密文时特殊字符被 shell 处理。当时数据库管理员给了一个临时密码,里面有#和$符号。执行命令时没有给密码加引号,shell 把后半段当成注释或变量解析,生成的密文对应的并不是完整密码。启动时解密成功,但数据库登录一直被拒绝。这个问题的教训是:命令行生成密文时,任何包含特殊字符的密码都要用双引号整体包住,对于生产密码,建议直接把密码写入临时环境变量再引用,避免出现在终端历史里。
4.3 密钥搞坏之后如何无损恢复
如果私钥丢了,或者公钥配错了,数据库里的数据并不会丢,受影响的是应用启动链路。最稳妥的处理方式是分情况操作:
- 公钥配错:只需要用正确公钥更新配置,不需要重新生成密文。
- 密文与公钥不匹配:需要拿到原始数据库密码,用私钥原样加密生成新密文。如果原私钥还能找到,这个过程在本地一分钟完成。
- 私钥完全丢失:这时候最安全的做法不只是重新生成密钥对,还应该考虑同时轮换数据库密码。因为私钥丢了不代表密码一定泄露,但从安全角度看,已经脱离了“可控”状态。建议 DBA 配合改一次密码,再用新私钥加密新密码。
无论哪种情况,都建议在配置中心保留一个可以快速回滚的历史版本。发布时先灰度一台实例,验证通过再全量推,避免出现“上线即全瘫”的场面。
5. 安全加固与影响范围分析
5.1 这一改动影响了哪些模块和流程
集成 Druid 密码加密,表面上看只是改了一行配置,实际上会给周边流程带来一系列变化。
连接池初始化环节是最直接受影响的。Druid 在启动阶段多了一步解密动作,虽然耗时通常可以忽略不计,但如果配置错误,应用启动会直接失败,所以这个改动必须走完整的发布验证流程。
配置管理侧的变化更明显:从今以后,项目仓库里不允许出现任何数据库明文密码;生产环境密码由 DBA 或指定负责人统一管理和生成;开发同学如果需要本地连库,也要走同样的加密流程,而不是图省事直接复制明文到本地配置。
CI/CD 流水线也需要同步关注:构建机上如果跑过生成密钥的命令,要确认私钥没有进入构建产物或日志;发布脚本不能把数据库明文密码作为参数传入。
对 DBA 来说,影响是正向的:数据库账号密码不再散落在各个开发者本地,密码变更后可以更可控地推动配置刷新。真正受影响的角色是那些手里拥有明文密码的人,他们需要把密码迁移到安全的密钥管理环境中。
5.2 结合配置中心与 CI/CD 的落地建议
我比较推荐的生产落地方式是:代码仓库只保存密文,公钥通过配置中心或环境变量注入。以 Spring Boot 为例,在application.yml里用占位符:
spring: datasource: druid: connection-properties: "config.decrypt=true;config.decrypt.key=${DB_PUBLIC_KEY}"这样即使有人把仓库打包下载,他也只看到一段密文和一个变量引用,无法直接连库。配置中心里可以按环境分别设置:
- 开发环境公钥放在开发配置中心群组。
- 生产环境公钥放在生产配置中心群组,且只有指定运维人员有修改权限。
CI/CD 侧还要增加一个探活动作,比如发布后自动执行一个简单的数据库查询接口,验证解密链路是否正常。如果连接失败,立即回滚配置。这一步很便宜,但能避免“发布到一半才发现密码配错”的尴尬。
5.3 还要做哪些配套加固
数据库密码加密只是数据库安全里的一环,别把它当成万能药。配套的加固至少还要做这几件:
- Druid 监控接口不要暴露到公网。如果开启了
StatViewServlet,一定要配置登录账号密码和 IP 白名单,很多泄露事故就是从监控后台撞进去的。 - 数据库账号权限最小化。应用账号只保留需要的增删改查权限,不要把 DBA 权限给到应用连接账号,这样即使连接信息泄露,损失也能控制在一定范围内。
- 配置文件的文件权限要收紧。生产服务器上的
application.yml不应该对所有人可读,建议由运维账号管理。 - 定期检查 Git 历史中是否还残留明文密码。如果有,不要只删除当前文件,要处理历史提交记录,并触发一次密钥轮换。
把加密机制和这些配套措施放在一起看,整个数据库凭据保护才算真正闭环。代码仓库里看到的不再是明晃晃的密码,而是经过非对称加密之后的密文,以及一把完全不敏感的公钥。
按我个人经验,Druid 这套加密方案最大的价值不是算法本身有多强,而是它把安全习惯嵌入了日常开发流程。第一次配置可能花二十分钟,之后每次新建环境都顺手做一遍,就不会再出现配置仓库里躺着一堆明文密码的尴尬。最后再补充一个容易被忽略的小技巧:生成密钥的时候顺手把时间、环境、负责人和私钥存放位置记到团队内部清单里,半年后轮换时会省非常多事。坚持用下来,数据库密码这扇门会扎实很多。