数据库密码明文写在配置文件里然后整个仓库一起推到远端,这事儿我在不少项目里见过,甚至有些人连生产库的密码也是这么裸着的。一旦仓库的访问权限没控制好,或者代码包被别人拿走了,数据库就等于直接送给对方,连爆破都省了。Druid 其实自带了一套成熟的 SQL 密码加密方案,配置起来一点都不复杂,核心就是官方提供的 ConfigTools 工具配合 RSA 非对称加密。这篇文章把我实际配置的过程、背后的原理、还有踩过的坑完整捋一遍,照着做基本十分钟之内就能让配置文件里的密码变成一串密文。
1. 为什么数据库密码要加密:一个配置文件的生死存亡
1.1 明文密码就像一个公开的钥匙盒
先聊一个最基本的场景:Spring Boot 项目里我们习惯把数据源配置放在 application.properties 或者 application.yml 里,最常见的就是这种写法。
spring.datasource.username=root spring.datasource.password=123456 spring.datasource.url=jdbc:mysql://10.0.0.5:3306/prod_db写的时候图省事,真到了出问题的时候才知道痛。之前我接手过一个项目,git 仓库设了公开权限,某天在做代码扫描的时候发现整个数据库连接串、账号、密码就在一个老版本的配置提交历史里躺着。虽然时间已经过去很久,密码早就换过了,但在那段时间里,任何拿到仓库的人都能直接连库。最可怕的不是密码本身,而是你根本不知道谁看过,也不知道数据有没有被悄悄拉走过。
你说那我把数据库 IP 设成内网地址不就行了吗?很多攻击是发生在内网横向渗透阶段的,一旦攻击者进入内网环境,扫描数据库端口、用配置里捞出来的账号密码直接连接,是非常常规的操作。所以把账号密码从配置文件里彻底解除出来,不是安全部门找茬,而是运维和开发的基本素养。
1.2 RSA 公私钥方案:Druid 官方加密机制的选型逻辑
Druid 的 ConfigTools 工具走的是 RSA 非对称加密路线。所谓非对称,就是加密和解密用的是不同的钥匙:公钥负责把明文密码加密成密文,私钥负责在运行时把密文还原成明文。公钥可以随便丢在配置文件里,因为公钥本身无法反向推导出私钥,别人拿到你的公钥和密文,在没有私钥的情况下也是解不开的。
对称加密用起来虽然简单,但问题也很明显:加密和解密用的是同一把密钥,那这把密钥放在哪里?放在配置文件里等于没加密,放在环境变量里又得保证所有部署节点同步。非对称加密的好处就体现出来了:公钥放仓库里不心疼,私钥放在部署机、环境变量或者配置中心里,access 严格控制,即使仓库整个泄露,数据库的真实密码仍然是安全的。
Druid 在连接池初始化的时候会读取 connection-properties 里的解密开关和公钥信息,再通过配置的私钥对密文进行解密,最终拿到明文去和数据库建立连接。这一整套流程对业务代码完全透明,你不需要改一行 DAO 代码,数据源照常注入,连接照常创建。
2. 实操:十分钟跑通 Druid 的 SQL 密码加密
2.1 准备工作:确认 Druid 版本和依赖
ConfigTools 的密码加密能力在比较新的版本里才稳定,建议至少使用 1.1.6 以上版本,当前推荐直接用 1.2.x 的最新版。版本太老的话,某些配置项可能不兼容,出了问题还不好查。如果你的项目已经在用 Druid 了,先确认 pom.xml 或者 gradle 里的版本号。
Maven 坐标示例:
<dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.2.8</version> </dependency>如果你用的是原生 druid 包而非 starter,也是一样的道理,只要 jar 里有com.alibaba.druid.filter.config.ConfigTools这个类,就可以继续往下走。
2.2 通过 ConfigTools 生成密钥对和密文
这一步是整个加密的核心前置操作。Druid 官方提供了一个现成的工具类,不需要你自己写 RSA 生成逻辑。用命令行直接执行:
java -cp druid-1.2.8.jar com.alibaba.druid.filter.config.ConfigTools your_plain_password把your_plain_password替换成你真实的数据库密码。执行之后,终端会输出三行关键内容:
privateKey:MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAL... publicKey:MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKJOxG... password:A9f4jf8sdf8sdf7...这里需要特别注意:password那一串才是用来替换配置文件里明文密码的密文,publicKey要放到配置文件的connection-properties里,privateKey不要写进会被提交到仓库的任何文件中,要放到部署环境的安全位置。
如果你的项目不方便执行这种命令行方式,也可以写个临时的 Java main 方法,直接调用 ConfigTools 的 main 入口,效果完全一样。
2.3 把密文和公钥写进配置项
以 Spring Boot 项目为例,修改 application.properties 中的数据源配置:
spring.datasource.druid.username=root spring.datasource.druid.password=上面输出的密文password spring.datasource.druid.connection-properties=config.decrypt=true;config.decrypt.key=上面输出的publicKey spring.datasource.druid.filters=config关键点在于这三项:
password一定要换成工具生成的密文,不要保留明文。connection-properties里用分号分隔两个参数:config.decrypt=true表示开启数据库密码解密,config.decrypt.key=公钥指定公钥内容。filters=config必须包含 config,这是触发解密功能的过滤器,漏掉它启动阶段就会报错。
如果你不是在 Spring Boot 环境里,而是手动创建 DruidDataSource,那配置方式也类似:
DruidDataSource dataSource = new DruidDataSource(); dataSource.setUrl("jdbc:mysql://10.0.0.5:3306/prod_db"); dataSource.setUsername("root"); dataSource.setPassword("上面输出的密文password"); Properties properties = new Properties(); properties.setProperty("config.decrypt", "true"); properties.setProperty("config.decrypt.key", "上面输出的publicKey"); dataSource.setConnectProperties(properties); dataSource.setFilters("config");这段代码里的逻辑和 Spring Boot 配置是完全对应的,核心就是设置连接属性和 filter。
2.4 验证加密是否真正生效
配置改完之后,不要直接上生产,先本地或者测试环境验证一遍。启动应用,观察控制台日志和数据库端的连接情况。
一个比较直接的验证方法:故意把配置里的 password 改成一串错误的密文,比如把最后一个字符改掉,然后重新启动应用。如果配置生效了,应用应该报解密失败或者说获取连接失败,而不是直接报 access denied 之类的数据库认证错误。如果还能正常连接数据库,说明你的 password 字段里还被某个解析器覆盖成了别的值,或者 filter 根本没生效。
我习惯的验证流程是:先正常启动一次,确认业务无异常;再故意破坏密文,确认启动会失败;最后改回正确密文,正式发布。这样能确保加密链路是通的,而不是自己骗自己。
3. 配置背后的细节原理:这样写才不容易翻车
3.1 filters=config 到底是干什么的
很多人配置完发现没效果,第一反应是公钥不对、密文不对,实际上最常犯的错是漏了 filters。Druid 的密码解密不是一个自动发生的过程,它需要经过一个叫做 config filter 的过滤器。filters 里没写 config,Druid 根本不会去解析 connection-properties 里的解密参数,密文就会被当成真实的密码直接拿去连数据库,结果必然报错。
如果你配置了多个 filter,比如统计、日志、慢查询,要写在一起并用逗号分隔:
spring.datasource.druid.filters=stat,config,slf4j注意顺序其实不重要,Druid 会按约定加载。但 config 这个必须出现,而且要保证你的 filter 列表覆盖了它。
3.2 公钥、私钥的正确分工与安全存放
生成出来的三串信息,很容易让人搞混。我再强调一遍用法,因为我见过有人把 privateKey 也写进配置文件的,那就等于没加密,或者说比加密前还多暴露了一把私钥。
- 公钥(publicKey):写进 application.properties 的
config.decrypt.key,可以随代码仓库走,可以公开。 - 私钥(privateKey):绝对不要进仓库,也不要写死在代码里。放到部署服务器的 /etc/druid/ 目录、环境变量、或者配置中心的安全项里,通过
${DRUID_PRIVATE_KEY}之类的占位符引用。 - 密文(password):替换数据库连接配置里的 password 项。
Spring Boot 里可以用环境变量加占位符的方式来引用私钥:
spring.datasource.druid.connection-properties=config.decrypt=true;config.decrypt.key=${DRUID_PUBLIC_KEY}有朋友会问,公钥都放配置文件里了,岂不是拿到公钥也能通过密文反推出明文?RSA 非对称加密在数学上保证了,通过公钥无法在合理时间内推导出私钥,反过来也一样。所以公钥公开没有实质风险,真正要保护的是私钥。
3.3 多环境隔离与密钥轮换
同一个项目可能有开发、测试、预发、生产四套环境,数据库密码各不相同,密钥对也应该各不相同。不要让所有环境共享同一把公钥和私钥,否则测试环境的密钥泄露了,生产环境也得跟着一起换。
比较好的实践是:每个环境生成独立的密钥对,分别维护密文和公钥。开发环境甚至可以直接用明文密码,方便本地调试,因为开发库风险可控;但从测试环境开始启用加密,确保部署链路和生产一致。
密钥轮换是另一个绕不开的话题。数据库密码一旦换掉,需要重新跑一遍 ConfigTools 生成新密文,然后用新密文替换配置,重启应用。如果公钥私钥也需要轮换,顺序上是:
- 先用旧私钥确认当前还能正常解密;
- 生成新的密钥对,基于新公钥加密新密码;
- 先把新密文和新公钥配到预发或灰度环境,确认没问题;
- 生产环境先切一半实例,观察连接池和业务指标正常后再全量替换;
- 最后清理旧私钥和旧密钥对的备份文件。
整个过程其实没有那么复杂,但一定要有记录,用表格记录每个环境对应的公钥指纹、密文生成时间、上次轮换时间,后面排查问题会省很多时间。
3.4 其他值得注意的配置细节
有些版本的 Druid 支持把公钥放到单独的配置文件里,然后通过 key 属性指定路径,比如:
config.decrypt.key=classpath:druid-key/public.key这种方式的好处是配置文件更干净,公钥文件和代码文件分离。但要注意 classpath 路径别写错,启动时如果找不到文件,同样会报错。
另外,有些项目里会同时配置多个数据源,比如多数据源分库分表场景。每个数据源都要单独设置 connection-properties 和 filters,不能只配主数据源就指望所有连接都被加密,这个坑比较隐蔽,排查起来也容易绕弯路。我曾经在一个多数据源项目里漏配了其中一个库的 config filter,结果那个库的密码一直是明文状态,后来做库表改造的时候才发现。
4. 常见报错与排查实录
4.1 启动直接报 invalid public key
这类报错一般长这样:
Caused by: java.security.NoSuchAlgorithmException: invalid public key原因是公钥内容不对。可能情况是复制公钥的时候漏了字符、多了换行,或者复制成了 privateKey。解决办法就是重新跑一次 ConfigTools,把新生成的公钥完整复制过去,注意前后不要有多余空格,yaml 文件里尤其容易因为缩进导致字符串被截断。
如果公钥本身没放错但还是报错,检查一下 connection-properties 的 key 名称。Druid 要求公钥配置项叫config.decrypt.key,不能写成publicKey或者decrypt.key,一旦名字对不上,解密流程拿不到公钥,就会报类似错误。
4.2 加了 config 之后仍然报密码错误
密码错误这个大方向下可以拆成几种情况:
- 密文复制错了,把 publicKey 当成密文写进 password 字段。这种情况数据库会收到一串长度很长的“密码”,连接失败。
- 密文是对的,但公钥和私钥不是同一对。比如你用了 A 环境生成的公钥,配了 B 环境生成的私钥去解,RSA 解密出来的结果完全是乱码,连库自然失败。
- 私钥没正确传进去。如果私钥通过环境变量引用,先确认 shell 里能不能 echo 出来。有些部署平台的环境变量里有特殊字符,会导致私钥内容被截断。
排查技巧:打开 debug 日志级别,观察日志里 Druid 解密阶段有没有输出明显的异常堆栈。如果堆栈里是 Cipher 相关的BadPaddingException,那基本就是密钥对不匹配或者密文被改过。
4.3 为什么换了一台机器就不行了
在本地明明配置得好好的,部署到新服务器上就启动失败。九成原因是私钥没有部署到那台机器上。
私钥如果放在某个文件路径里,比如 /opt/keys/private_key.pem,那每台部署机器上都得有这个文件,还要保证运行应用的系统用户对这个文件有读取权限。很多环境问题都是权限问题:文件存在,但应用进程读不了。
如果是通过环境变量注入,要检查部署平台的环境变量配置是不是在构建阶段被覆盖了。比如同一个镜像在不同的部署环境里用同一套环境变量,那密钥就得按环境单独指定。
4.4 日志里打印密文和明文
还有一个很容易被忽略的安全细节:日志打印。有些项目会在启动阶段打印数据源配置,或者配置中心拉取配置的时候把整个数据源内容输出到日志里。如果你在 application.yml 里打开过logging.level.com.alibaba.druid=debug,Druid 内部连接数据库之前的一些信息可能会被打印出来,里面的 password 字段暴露了密文事小,如果某段逻辑不小心在解密后打印了明文,那风险就大了。
建议在排查问题的时候临时开 debug,排查完立刻调回 warn 或 info。另外,连接池监控页面里如果配置了展示密码之类的功能,也要在对外暴露前关掉或者脱敏。
4.5 版本升级带来的兼容问题
老项目从 Druid 1.0.x 往上升级的时候,可能遇到加密参数不识别的问题。1.0.x 版本的 config filter 还不支持后来引入的一些配置格式,需要直接升级到 1.1.x 或者 1.2.x 才能正常使用。升级依赖之后,建议先跑一遍全量测试,特别是多数据源和分页、事务相关的场景,因为连接池版本变化可能影响初始化和连接获取逻辑。
还有一个不太常见但确实存在的情况:项目中同时引用了多个版本的 druid jar 包,比如某个公共组件内部自带了一套旧版 druid,导致应用实际加载的 ConfigTools 类和预期的不一致。这种包冲突问题可以用依赖树分析,或者直接排除传递依赖,再显式指定一个版本。
4.6 connection-properties 里的特殊字符
connection-properties 是字符串形式的,多个属性之间用分号分隔。如果公钥里有特殊字符,比如加号、斜杠、等号,在 yaml 或 properties 文件里可能被解析器处理掉。建议把公钥用双引号包起来:
spring.datasource.druid.connection-properties="config.decrypt=true;config.decrypt.key=MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKJOxG..."如果还是有问题,把公钥放到外部文件里用路径引用,能有效绕开特殊字符问题。
在我实际配过的项目里,最折腾的一次就是在 Kubernetes 环境里通过 configmap 挂载 properties 文件,公钥里的+号被环境变量渲染的时候转成了空格,导致所有环境全部启动失败。排查这个问题的办法是把配置内容打进镜像之前先 base64 解码看一遍,确认特殊字符没有被吞。
5. 后面还可以怎么扩展
5.1 结合配置中心统一管理密钥
用 Nacos 或类似的配置中心来管理数据源配置的话,可以把公钥、密文都放到配置中心,只有运维人员有权限修改。私钥可以存在每个应用所在机器本地,这样配置中心就算被人扒了,也拿不到私钥,数据源照样安全。配置中心本身就支持版本回滚,切换密钥的时候还能快速回退,比直接改配置文件要方便得多。
5.2 Jasypt 并不是唯一选择
做 Java 项目的朋友可能会想到另一个密码加密库 Jasypt,它也能对 Spring Boot 配置里的密码进行加密。Jasypt 的优点是使用起来对配置属性几乎无侵入,可以把任意配置项的明文变成 cipher 字符串。但 Jasypt 默认的加密方式需要你把密钥(secret)以明文形式也放在配置文件或者环境变量里,它本身不解决密钥管理问题。
Druid 自带方案的独特优势是:解密动作和连接池绑定得更紧,RSA 非对称机制避免了 Jasypt 的对称密钥难题。如果你已经在用 Druid,没必要额外引一套 Jasypt,直接用 ConfigTools 就好。如果有些密码不是数据库连接用的,而是类似 Redis 密码,也可以考虑用 Jasypt 单独处理,但那就是另一套方案了。
5.3 密钥管理和安全审计
配置加密这件事,本质上不是为了防住百分百的攻击,而是把风险控制在一个可控的范围。数据库密码只是一个起点,整个配置体系里往往还有消息队列密码、Redis 密码、第三方平台密钥。如果都处于明文状态,那安全建设的价值就大打折扣。
建议每个项目都建立一张配置加密登记表,记录配置项名称、加密方式、密钥存放位置、轮换周期。等到安全审计的时候,这一张表就是你最有说服力的整改证据,比临时截图强太多了。
我个人的体会是,Druid 密码加密这个功能,第一次配的时候稍微有点绕,但真正跑通之后,后面所有数据库连接都走同一套流程,心理负担会小很多。你不需要记住那一串 RSA 密钥原理的细节,只需要知道:公钥进配置,私钥进环境,密文替换密码,filters 别漏,这四句话能帮你解决八成以上的问题。后面如果遇到了更复杂的场景,比如多数据源、Kubernetes 部署、密钥轮换,再回到这几个关键细节上查,基本都能找到原因。