
1. 项目概述为什么数据源密码加密是生产环境的“标配”最近在排查一个线上项目的安全审计报告时又被老生常谈的问题点了一下“数据库连接密码明文存储”。这让我想起几年前一个真实的案例某公司因为配置文件泄露导致数据库被拖库损失惨重。从那以后但凡我经手的Java项目只要用到了数据库连接池数据源密码加密就成了部署前的最后一道必检工序。今天要聊的就是如何为阿里巴巴开源的Druid连接池实现数据源密码的加密。Druid本身是一个功能强大的数据库连接池和监控组件但它和大多数基础组件一样默认配置下数据库密码spring.datasource.password是以明文形式写在application.yml或application.properties文件里的。这对于任何需要将代码提交到Git仓库或者配置文件可能被运维、测试等多角色接触的生产环境来说都是一个潜在的安全风险。加密的目的不是为了防止黑客攻破服务器那层面安全是另一回事而是为了在开发协作、配置管理、日志输出等环节避免密码因疏忽而泄露。所以这个“实现druid数据源密码加密”的需求核心目标很明确在保持Druid连接池所有功能正常的前提下将配置文件中那个明文的密码替换为一串加密后的密文。系统启动时Druid能自动识别这是一串密文并用我们预设的密钥进行解密拿到真正的密码去建立数据库连接。整个过程对业务代码应该是透明的即你的DataSourceBean和JdbcTemplate的使用方式不需要做任何改变。2. 方案选型与设计思路对称加密是主流选择要实现这个目标我们得先选一个加密方案。市面上加密方式很多但适合配置文件密码加密的需要满足几个条件首先它必须是可逆的对称加密因为程序启动时需要解密得到原文其次加解密速度要快不能明显影响应用启动时间最后实现要简单最好能与Spring Boot和Druid无缝集成。2.1 主流方案对比Druid内置加密 vs. 自定义加解密Druid其实早就考虑到了这一点它提供了一套内置的加密解密工具。同时我们也可以利用Spring的BeanPostProcessor等扩展点来自定义加解密逻辑。我们来简单对比一下特性Druid内置加密 (druid-spring-boot-starter集成)自定义加解密 (如使用BeanPostProcessor)实现复杂度低。主要工作是生成密文和修改配置。中高。需要自己编写解密逻辑并注入到DataSource生命週期中。与Druid集成度高。原生支持通过配置项即可开启。一般。需要手动干预Bean的创建过程。灵活性一般。通常使用固定的算法如AES密钥管理方式固定。高。可以自由选择加密算法如RSA、国密SM4自定义密钥存储方式如从环境变量、配置中心读取。维护成本低。遵循标准配置社区方案成熟。中。需要自行维护加解密组件并确保与Spring版本兼容。推荐场景绝大多数标准Spring Boot项目追求快速落地。有特殊安全合规要求如必须使用特定算法或已有统一的配置加解密框架。对于90%以上的项目我强烈推荐使用Druid内置的加密方案。它足够简单、稳定并且是“官方推荐”的玩法出了问题也容易搜索到解决方案。我们今天也将以这种方案为主线进行详解。2.2 核心设计思路图解整个流程可以概括为“两步走”线下加密线上解密。准备阶段线下利用Druid提供的工具类使用一个只有运维和核心开发知道的“私钥”对数据库明文密码进行加密得到一串密文。运行阶段线上在Spring Boot的配置文件中不再填写明文密码而是填写上一步得到的密文。同时通过配置告诉Druid“我这个密码是加密过的解密私钥是XXX”。Druid在初始化DataSource时会自动完成解密工作。注意这里说的“私钥”在对称加密中更准确的叫法是“密钥”(key)。整个安全性的基石就在于这个密钥的保管而不是加密算法本身。绝对不能把密钥和密文放在同一个Git仓库里常见的做法是将密钥放在运维手中的环境变量、启动参数或者专用的配置中心/密钥管理服务如HashiCorp Vault, AWS KMS中。3. 实操详解基于Druid Spring Boot Starter实现加密接下来我们进入手把手实操环节。假设我们有一个标准的Spring Boot 2.x/3.x项目已经引入了druid-spring-boot-starter依赖。3.1 第一步生成密码密文在将密码写入配置文件之前我们需要先把它加密。Druid提供了一个非常方便的命令行工具类com.alibaba.druid.filter.config.ConfigTools。你可以写一个简单的Java类或者直接在你的测试代码里运行它。这里给出一个最常用的示例使用RSA算法实际上Druid内部使用RSA生成密钥对然后用公钥加密私钥解密但对我们使用者而言可以简化为用同一个密码-p参数进行对称加解密其底层是安全的随机密钥加密方式。import com.alibaba.druid.filter.config.ConfigTools; public class PasswordEncryptor { public static void main(String[] args) throws Exception { // 你的数据库明文密码 String plainPassword MySuperSecretPassword123!; // 一个用于加密的“私钥”任意复杂字符串请务必牢记 String privateKey YourPrivateKeyHere-2024; // 使用ConfigTools.encrypt方法加密 String[] keyPair ConfigTools.genKeyPair(512); // 生成RSA密钥对512位强度已足够 System.out.println(PrivateKey: keyPair[0]); System.out.println(PublicKey: keyPair[1]); // 使用公钥加密密码 String encryptedPassword ConfigTools.encrypt(keyPair[0], plainPassword); System.out.println(Encrypted Password: encryptedPassword); // 验证使用私钥解密可选用于验证 String decryptedPassword ConfigTools.decrypt(keyPair[1], encryptedPassword); System.out.println(Decrypted Password: decryptedPassword); System.out.println(Match: plainPassword.equals(decryptedPassword)); } }运行这段代码你会在控制台看到输出。其中encryptedPassword就是我们需要替换到配置文件里的密文。而privateKey和publicKey就是我们需要妥善保管的密钥对。实操心得 在实际操作中我更喜欢用单元测试的方式来生成密文这样可以把生成逻辑和验证逻辑都写在一起方便后续密码变更时复用。同时务必在生成后立即将明文密码从代码中删除这个生成动作应该在开发或运维的本地环境执行切勿在提交的代码中留下明文密码或固定的私钥。3.2 第二步修改Spring Boot配置文件拿到密文后我们就要改造配置文件了。这里以application.yml为例properties文件逻辑类似。加密前的配置危险spring: datasource: url: jdbc:mysql://localhost:3306/my_db?useSSLfalseserverTimezoneUTC username: root password: MySuperSecretPassword123! # 明文密码 driver-class-name: com.mysql.cj.jdbc.Driver type: com.alibaba.druid.pool.DruidDataSource druid: initial-size: 5 min-idle: 5 max-active: 20加密后的配置安全spring: datasource: url: jdbc:mysql://localhost:3306/my_db?useSSLfalseserverTimezoneUTC username: root password: OW6fP6pFcLx0JtC3v4mzK/kpE0lR6A3Xqo7bBkLpKbM # 这里是加密后的密文 driver-class-name: com.mysql.cj.jdbc.Driver type: com.alibaba.druid.pool.DruidDataSource druid: # 连接池通用配置 initial-size: 5 min-idle: 5 max-active: 20 # 核心加密配置在这里 connection-properties: config.decrypttrue;config.decrypt.key${DATASOURCE_PUBLIC_KEY:你的公钥字符串} filter: config关键改动有两处password字段的值替换成了我们第一步生成的密文。在druid配置下增加了connection-properties和filter配置。filter: config这启用了Druid的ConfigFilter它是负责解密的核心过滤器。connection-properties: config.decrypttrue;config.decrypt.key...这个属性告诉ConfigFilter“这个数据源的密码需要解密解密用的公钥是...”。重要提示注意这里用的是config.decrypt.key它对应的是公钥PublicKey而不是私钥。这是因为Druid的ConfigTools默认使用RSA非对称加密公钥用于加密我们这里用来“配置解密”其实是个反向操作更准确说是用私钥加密公钥解密私钥用于解密。但在配置层面我们提供公钥给Druid去验证和解密密码。千万不要把私钥放在这里3.3 第三步安全地管理解密公钥把公钥直接写在application.yml里虽然密码加密了但公钥仍然暴露了。为了更安全最佳实践是将公钥放在环境变量或启动参数中。connection-properties: config.decrypttrue;config.decrypt.key${DATASOURCE_PUBLIC_KEY}然后在启动应用时传入环境变量DATASOURCE_PUBLIC_KEY你的公钥字符串 java -jar your-app.jar或者使用启动参数Spring Boot 支持java -jar your-app.jar --spring.datasource.druid.connection-propertiesconfig.decrypttrue;config.decrypt.key你的公钥字符串对于生产环境更推荐使用配置中心如Nacos, Apollo来管理这个配置项或者使用专业的密钥管理服务。这样配置文件里就彻底没有敏感信息了。4. 深度原理与进阶配置4.1 ConfigFilter是如何工作的很多朋友配置完能用但心里不踏实不知道底层发生了什么。简单来说当DruidDataSource在初始化时会加载所有配置的Filter。ConfigFilter被加载后它会检查connection-properties中是否包含config.decrypttrue。如果为true它会取出spring.datasource.password的值此时是密文。使用config.decrypt.key指定的公钥调用ConfigTools.decrypt()方法进行解密。将解密后的真实密码设置回DataSource的属性中用于后续创建物理数据库连接。同时它还会在内存中将password属性值替换为空字符串这是一个非常重要的安全特性防止密码在内存中被意外dump出来。整个过程发生在Spring容器创建DataSourceBean的早期对业务完全无感。4.2 多数据源场景下的加密配置现在微服务架构下一个应用连接多个数据库的情况很常见。在多数据源配置中加密的写法需要稍作调整。假设我们有两个数据源primary和secondary。Configuration public class DataSourceConfig { Bean ConfigurationProperties(spring.datasource.druid.primary) public DataSource primaryDataSource() { // 这里返回的DruidDataSource会自动绑定以spring.datasource.druid.primary为前缀的配置 return DruidDataSourceBuilder.create().build(); } Bean ConfigurationProperties(spring.datasource.druid.secondary) public DataSource secondaryDataSource() { return DruidDataSourceBuilder.create().build(); } }对应的application.yml配置spring: datasource: druid: primary: url: jdbc:mysql://host1:3306/db1 username: user1 password: ENC(密文1) # 也可以使用ENC()包裹需额外配置 driver-class-name: com.mysql.cj.jdbc.Driver # 每个数据源单独配置加密属性 connection-properties: config.decrypttrue;config.decrypt.key${PUB_KEY_PRIMARY} filter: config secondary: url: jdbc:mysql://host2:3306/db2 username: user2 password: ENC(密文2) driver-class-name: com.mysql.cj.jdbc.Driver connection-properties: config.decrypttrue;config.decrypt.key${PUB_KEY_SECONDARY} # 甚至可以使用不同的密钥 filter: config # 全局的druid监控等配置可以放在这里 stat-view-servlet: enabled: true关键在于每个数据源的connection-properties和filter配置都需要独立设定。如果你使用ConfigurationProperties绑定确保这些配置项能被正确绑定到每个DruidDataSource实例上。4.3 与Jasypt等集成加密方案的对比与选择社区里另一个非常流行的配置加密方案是Jasypt。它功能更通用可以对配置文件里任意字符串进行加密不仅仅是密码并且支持多种加密算法。你可能会问为什么不直接用JasyptJasypt方式通常使用ENC(密文)的格式包裹配置值并配合一个StringEncryptorBean在配置加载阶段全局解密。它更独立不依赖具体的数据源实现。Druid内置方式专为Druid数据源密码设计解密时机更晚在Druid初始化时且具有内存中擦除密码的安全特性。如何选择如果项目只关心数据库密码加密且确定使用Druid那么Druid内置方案更直接、更轻量没有额外的依赖和Bean定义。如果项目需要对大量配置项如Redis密码、MQ连接串、第三方API密钥进行加密那么引入Jasypt进行统一管理是更好的选择避免每种组件都搞一套加密方案。两者也可以共存但一般没必要。我个人的经验是中小型项目用Druid内置加密足矣大型项目或有严格安全审计要求的可能会采用统一的配置加密服务此时Druid的密码也是通过该服务解密后注入的。5. 常见问题排查与实战技巧即使按照步骤操作你也可能会遇到一些坑。下面是我在多次实践中总结的常见问题及解决方法。5.1 启动报错java.sql.SQLException: null, message from server: Access denied for user rootlocalhost (using password: YES)问题现象应用启动失败抛出数据库连接异常提示密码错误。排查思路检查密文和公钥是否匹配这是最常见的原因。确保你用来解密的公钥和生成密文时使用的密钥对是同一对。重新运行加密程序使用正确的公钥进行验证。检查密文是否被破坏YAML/Properties文件对特殊字符如:{,}有转义要求。如果密文中包含这些字符可能需要用引号包裹。在YAML中最好将密文用单引号括起来防止转义。password: OW6fP6pFcLx0JtC3v4mzK/kpE0lR6A3Xqo7bBkLpKbM检查配置项路径确保connection-properties和filter配置是写在spring.datasource.druid节点下而不是spring.datasource直接子节点。使用spring.datasource.druid.*的配置方式是最稳妥的。开启Druid日志在application.yml中增加以下配置查看ConfigFilter的详细处理日志。logging: level: com.alibaba.druid: DEBUG启动时如果看到druid filter config init相关的日志并且打印了decrypt password等信息说明过滤器已生效。如果没看到说明配置可能没被加载。5.2 监控页面或日志中依然显示明文密码问题现象应用运行正常但在Druid内置的监控页面 (/druid/sql.html) 的“数据源”选项卡或者在应用日志中打印数据源信息时看到了明文密码。原因与解决 这是一个误解。Druid的ConfigFilter在解密密码并建立连接后确实会将内存中的password属性置空。但是Spring Boot在启动时会先将配置文件中的值密文加载到Environment中然后才创建DataSource。有些监控组件或日志打印的是Environment中的原始值或者是DataSourceBean初始化前的某个快照。如何确认密码是否安全你可以写一个简单的RestController通过/actuator/env端点需引入Spring Boot Actuator查看spring.datasource.password这里显示的是密文。更直接的办法是在运行时通过Java代码获取DataSource的password属性Autowired private DataSource dataSource; GetMapping(/checkPassword) public String checkPassword() throws SQLException { if (dataSource instanceof DruidDataSource) { DruidDataSource ds (DruidDataSource) dataSource; // 尝试获取密码ConfigFilter处理后这里应该是空字符串或null String rawPassword ds.getPassword(); return Password in DruidDataSource object is: [ rawPassword ]; } return Not a DruidDataSource; }访问这个接口如果返回的密码是空字符串或null说明ConfigFilter生效了内存中的密码已被清除这是安全的。监控页面显示的可能是其他环节缓存的信息不代表当前内存中存在明文密码。5.3 在Spring Boot 3.x或最新版本Druid中的注意事项Spring Boot 3.x和Druid的最新版本如1.2.20兼容性很好但配置方式有细微变化推荐使用druid-spring-boot-3-starter。加密配置的核心原理不变但要注意依赖写法dependency groupIdcom.alibaba/groupId artifactIddruid-spring-boot-3-starter/artifactId version1.2.20/version /dependency配置格式基本保持不变。但有一点需要警惕Spring Boot 3.x对配置文件的处理和环境变量的绑定更加严格确保你的connection-properties字符串格式正确特别是当值中包含等号和分号;时YAML解析器可能会出问题。如果遇到诡异问题尝试将connection-properties作为一个多行字符串或者使用Properties文件格式。5.4 密钥轮换与密码更新策略密码和密钥不是一成不变的。当需要更新数据库密码时流程应该是用新的密钥对或沿用旧公钥加密新密码生成新密文。更新配置文件中的password值为新密文。如果更换了密钥对还需要同步更新config.decrypt.key的值或对应的环境变量。重启应用。为了平滑过渡可以在数据库层面先同时设置新旧密码等应用全部重启完成后再移除旧密码。密钥的保管必须通过安全的渠道传达给所有需要部署应用的人员或自动化部署系统。我个人在实际操作中的体会是数据源密码加密这个事属于“不做没事出了事就是大事”的基础安全措施。它的实施成本很低但带来的安全收益是实实在在的。尤其是在DevOps流程中配置文件难免会在不同环境、不同人员间流转加密能有效降低敏感信息暴露的风险。最后再分享一个小技巧在团队内部可以将密码加密/解密的操作封装成一个简单的命令行工具或脚本并写好文档这样无论是新人上手还是日常维护都能规范操作避免出错。