告别明文密码:Spring Boot集成Druid实现AES加密
1. 数据库密码放配置文件到底有多危险先讲个真实场景。某项目上线前做安全巡检运维顺手翻了一下配置仓库发现application.yml里明晃晃躺着一行spring.datasource.passwordroot123456不只是测试环境生产环境也这么干。当时所有人都觉得无所谓反正数据库在内网防火墙挡着别人又连不进来。结果没过多久一个外包开发把整个仓库打包带走了数据库地址、账号、密码全在里面等于把生产库的钥匙直接送了人。这种事不是个例。Spring Boot 项目里数据库密码明文写在配置文件里是最常见的安全隐患没有之一。常规的 Git 仓库、CI 日志、代码分享平台都会成为泄露渠道。密码一旦泄露轻则数据被删重则整个业务被拖库勒索。所以这次我做了一个很实际的事把 Spring Boot 和 Druid 连接池集成起来同时对数据库密码做加密处理让配置文件里永远不会出现真实密码。有人可能会说现在用配置中心不就解决了吗。配置中心只是把密码挪到了另一个地方本质上还是明文存储。而且很多中小项目根本没上配置中心配置文件依然是唯一配置来源。密码加密这件事和配置放哪里没关系属于数据库访问层的安全加固值得每个 Spring Boot 项目都做。Druid 连接池在 Java 生态里用得很多阿里巴巴开源功能比 HikariCP 丰富不少自带监控、防 SQL 注入、慢查询日志这些能力而且它内置了密码加密工具官方设计上就支持这个需求。但内置方案有一些使用成本很多人配置到一半就放弃了。这篇我会先讲清楚为什么密码会泄露、泄露之后会发生什么再给出完整可跑的集成方案以及我在生产环境里踩过的坑和总结出的自定义加密思路。整个过程不复杂核心就三步配置 Druid 数据源、生成并管理密钥、在数据源初始化时解密密码。但做的时候你会发现加密本身好做密钥怎么藏、加解密逻辑放在哪一层、怎么避免解密日志把密码打出来才是真正需要花心思的地方。这篇内容适合正在用 Spring Boot 写业务、还没处理过数据库密码安全问题的开发也适合想做数据源规范化改造但没找到完整案例的团队参考。跟着做完你的配置文件里对应位置会变成一串密文意义就完全不同了。2. 为什么选择 Druid 而不是 HikariCP2.1 Spring Boot 默认数据源的短板Spring Boot 2.x 开始默认数据源是 HikariCP理由是它快性能测试里表现确实稳。但 Hikari 的设计哲学是极简不提供内置的密码加密能力也不带 Web 监控界面SQL 分析这块基本靠外部工具补齐。这里先明确一个概念数据库密码加密听起来像是一个高级功能但本质上是数据源在做 Connection 初始化时需要拿到真实密码去建立连接。我们做的事情是对配置文件里的密码先做一层转换让真实密码只存在于内存中而且是短暂存在于连接建立的过程中。Hikari 想做这件事也不是不行需要自己实现一个解密组件然后在spring.datasource配置里动态处理。但它的配置加载机制比较直接动态解密只能在 Java 配置类里绕配合 YAML 映射的时候很容易踩坑。Druid 就友好很多它官方就给了加密工具和配置项等于整条链路是现成的。2.2 Druid 集成能多拿到哪些东西选择 Druid 不只是为了加密顺带还能拿到几样对实际运维有帮助的能力内置连接池监控访问/druid/index.html能直接看活跃连接数、慢 SQL、事务耗时支持 SQL 防火墙和权限过滤可以拦截部分危险 SQL自带ConfigTools密码加密工具生成密文和公钥、私钥一套流程对 Spring Boot 的自动配置有专门适配集成成本低当然Druid 对比 Hikari 在极致并发下吞吐量稍有差距但对绝大多数业务系统来说这个差距远小于它带来的运维便利。取舍标准很简单如果你的场景是超高并发网关服务追求单机极限性能那继续用 Hikari 没问题如果是业务系统、管理后台、中台服务Druid 的综合收益更高。2.3 我需要它帮我解决什么问题我归纳了一下这个问题分三层第一层配置文件里的密码是明文仓库泄露等于数据库裸奔。这一层靠加密解决。第二层即使密文泄露也不能被轻易逆推出原文。这一层靠密钥管理和加密算法强度解决Druid 默认用 RSA非对称加密即使拿到公钥也无法反推私钥。第三层即使密钥也被泄露攻击者也不能用我们程序的方式直接连数据库。这一层就要靠自定义加密方案——比如在 RSA 外面再叠加一个系统环境变量里的随机盐或者干脆用对称加密把密钥放在环境变量里而不是写在仓库内。这三层做完数据库密码的安全等级才能算真正拉起来。接下来我从 Druid 的官方加密方式讲起再引出我推荐的自定义扩展方案。3. Druid 官方 ConfigTools 加密以及它不够用的地方3.1 官方方案的完整接入过程Druid 提供了一个工具类com.alibaba.druid.filter.config.ConfigTools核心方法是encrypt和decrypt底层用的是 RSA 非对称加密。接入过程分三步第一步先生成密钥对和密文。Druid 的 jar 包可以直接在命令行调用java -cp druid-1.2.20.jar com.alibaba.druid.filter.config.ConfigTools root123456执行之后会输出三样东西privateKey、publicKey、password密文。把这三个值保存下来其中私钥只用于生成时的验证实际运行环境不需要保留。第二步配置数据源。在application.yml里这样写spring: datasource: type: com.alibaba.druid.pool.DruidDataSource url: jdbc:mysql://127.0.0.1:3306/demo?useUnicodetruecharacterEncodingutf8 username: root password: GxrTXd5eQ0GfZixdFGsYaM0xxh3hGMK0f9sJ3fWx driver-class-name: com.mysql.cj.jdbc.Driver druid: filters: config connection-properties: config.decrypttrue;config.decrypt.key${druid.public-key}同时把公钥放到配置里或者放到环境变量里druid: public-key: MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAXX...公钥在 RSA 体系里本来就是公开的放配置文件没问题。真正要保护的是私钥但在运行环境里Druid 解密只需要公钥所以理论上即使整个仓库被拖走攻击者拿到了密文和公钥依然解不出原文因为 RSA 私钥才能解密。第三步需要一个显式的DruidDataSource配置类因为 Spring Boot 的自动配置默认走 Hikari。直接用一个Configuration类创建数据源Configuration public class DruidConfig { Value(${spring.datasource.url}) private String url; Value(${spring.datasource.username}) private String username; Value(${spring.datasource.password}) private String password; Value(${druid.public-key}) private String publicKey; Bean Primary public DataSource dataSource() throws Exception { DruidDataSource dataSource new DruidDataSource(); dataSource.setUrl(url); dataSource.setUsername(username); dataSource.setPassword(password); dataSource.setDriverClassName(com.mysql.cj.jdbc.Driver); dataSource.setFilters(config); dataSource.setConnectionProperties(config.decrypttrue;config.decrypt.key publicKey); return dataSource; } }这样跑起来Druid 会通过config过滤器读取config.decrypt配置在创建数据库连接前完成解密。官方方案到这里是能跑通的。3.2 实测下来你会发现三个问题我按照官方方案跑通之后在实际使用中发现了三个比较尴尬的点第一私钥出现过。生成密钥对的过程中私钥要么在命令行输出里要么在你往数据库配置里填的时候出现。如果是和外包协作开发或者流水线日志没过滤私钥很容易被带出去。虽然运行环境不需要私钥但私钥一旦泄露攻击者可以用它配合密文反推出原始密码。第二密钥对固定之后改密码成本高。数据库密码调整一次就需要重新跑一遍 ConfigTools 生成新的密文和公钥然后更新配置。如果数据库有主从或者多个环境同时部署这个操作很容易出乱子漏改一个节点就可能导致连接失败。第三公钥以明文形式存在于配置中依赖 RSA 本身的安全性。如果 RSA 密钥长度配置不当或者攻击者同样持有公钥和密文进行离线破解会有一定风险。虽然小概率但作为一个安全优化项目来说总觉得还可以再加强。3.3 我的结论官方方案用但要改造官方方案的核心思路是对的把密码变成密文运行时再解密。但它把安全边界放在了仓库不会被完整拖走这个假设上。很多公司的仓库是全量分享给团队内部的这种情况下 RSA 密钥对需要更谨慎的管理。我做的事情是在官方方案基础上增加一个动态盐值的自定义加密层。加密逻辑不再只依赖 RSA而是同时对密码内容加入环境变量中存储的盐值做对称校验解密时要求密钥、盐值、密文三者匹配。这样即使攻击者同时拿到仓库和公钥他解出来的也不是真实密码而是一个加了盐的中间值还得再拿到环境变量里的盐才能拼出原文。先说清楚一点Druid 官方方案本身够用如果你是个人项目或者内部系统按官方方案配置完注意私钥销毁问题不大。但如果要做企业级安全改造建议继续看下面的自定义方案它把密钥管理和密码变更的维护成本降了很多。4. 自定义数据库密码加密方案AES 环境变量盐4.1 整体设计思路我最终采用的结构是AES 对称加密 固定盐 环境变量密钥。配置文件中只存 AES 密文密钥本体放在服务器的环境变量里Druid 数据源在 Spring 容器初始化阶段完成解密。为什么不用 RSA 用 AES对称加密的加解密速度快适合在连接池初始化这种低频场景下调用每次启动只解一次性能无压力。而对称加密真正的问题从来不是算法本身而是密钥怎么藏。放到环境变量就绕开了配置文件仓库泄露的问题。整体流程是这样有一个本地工具类专门用来生成密文输入明文数据库密码输出 AES 密文密文写入application.yml真实的 AES 密钥写到生产服务器的~/.bashrc或系统环境变量Spring Boot 启动时数据源配置类读取环境变量DB_PASSWORD_KEY用它对密文解密解密结果仅在内存中用于创建连接这个设计下仓库泄露只会暴露密文而密文没有 AES 密钥是解不开的。即使攻击者物理拿到服务器也需要同时具备系统登录权限和数据库密码密文安全等级比官方方案高出一截。4.2 AES 加解密工具类实现这里用 Java 原生 API 写一个简单的 AES 工具。先把加密逻辑写清楚package com.example.common.util; import javax.crypto.Cipher; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import java.nio.charset.StandardCharsets; import java.security.SecureRandom; import java.util.Base64; public class AesUtil { // AES/CBC/PKCS5Padding 加密模式 private static final String TRANSFORMATION AES/CBC/PKCS5Padding; // 固定长度 IV这里用 16 字节 private static final int IV_LENGTH 16; // 生成一个随机的 AES 密钥外部调用一次生成后记录到环境变量 public static String generateKey() throws Exception { byte[] keyBytes new byte[16]; SecureRandom secureRandom new SecureRandom(); secureRandom.nextBytes(keyBytes); return Base64.getEncoder().encodeToString(keyBytes); } public static String encrypt(String plainText, String base64Key) throws Exception { byte[] keyBytes Base64.getDecoder().decode(base64Key); SecretKeySpec secretKeySpec new SecretKeySpec(keyBytes, AES); // 随机 IV随密文一起返回解密时提取 byte[] ivBytes new byte[IV_LENGTH]; SecureRandom secureRandom new SecureRandom(); secureRandom.nextBytes(ivBytes); IvParameterSpec ivSpec new IvParameterSpec(ivBytes); Cipher cipher Cipher.getInstance(TRANSFORMATION); cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivSpec); byte[] encryptedBytes cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8)); // 结构IV 密文一起 Base64 byte[] combined new byte[ivBytes.length encryptedBytes.length]; System.arraycopy(ivBytes, 0, combined, 0, ivBytes.length); System.arraycopy(encryptedBytes, 0, combined, ivBytes.length, encryptedBytes.length); return Base64.getEncoder().encodeToString(combined); } public static String decrypt(String encryptedBase64, String base64Key) throws Exception { byte[] combined Base64.getDecoder().decode(encryptedBase64); // 前 16 字节是 IV byte[] ivBytes new byte[IV_LENGTH]; byte[] encryptedBytes new byte[combined.length - IV_LENGTH]; System.arraycopy(combined, 0, ivBytes, 0, IV_LENGTH); System.arraycopy(combined, IV_LENGTH, encryptedBytes, 0, encryptedBytes.length); IvParameterSpec ivSpec new IvParameterSpec(ivBytes); byte[] keyBytes Base64.getDecoder().decode(base64Key); SecretKeySpec secretKeySpec new SecretKeySpec(keyBytes, AES); Cipher cipher Cipher.getInstance(TRANSFORMATION); cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivSpec); byte[] decryptedBytes cipher.doFinal(encryptedBytes); return new String(decryptedBytes, StandardCharsets.UTF_8); } }AES 用 CBC 模式IV 不固定加密时生成随机 IV 并拼在密文前面解密时先切片再解密。这一步的用意是同一明文在不同时间加密出来的密文完全不同避免攻击者通过多次加密比对猜测明文。4.3 如何把密文写进配置并动态解密拿到工具类之后生成一个密文public class GenerateDbPassword { public static void main(String[] args) throws Exception { String key YOUR_BASE64_AES_KEY; // 来自 generateKey() 的一次性输出 System.out.println(AesUtil.encrypt(root123456, key)); } }把输出结果放入application.ymlspring: datasource: url: jdbc:mysql://127.0.0.1:3306/demo username: root password: ENC(U2FsdGVkX1uBf2P8V1kPq5hZbWzbM9oV/XyO3cRIA) driver-class-name: com.mysql.cj.jdbc.Driver这里我加了一个ENC(...)前缀方便从格式上识别哪些配置是密文。然后在 Druid 的配置类里做解密Configuration public class DruidConfig { private static final String ENC_PREFIX ENC(; private static final String ENC_SUFFIX ); Value(${spring.datasource.url}) private String url; Value(${spring.datasource.username}) private String username; Value(${spring.datasource.password}) private String password; Bean Primary public DataSource dataSource() throws Exception { DruidDataSource dataSource new DruidDataSource(); dataSource.setUrl(url); dataSource.setUsername(username); dataSource.setPassword(resolvePassword(password)); return dataSource; } private String resolvePassword(String encryptedValue) { if (encryptedValue ! null encryptedValue.startsWith(ENC_PREFIX) encryptedValue.endsWith(ENC_SUFFIX)) { String inner encryptedValue.substring(ENC_PREFIX.length(), encryptedValue.length() - ENC_SUFFIX.length()); String key System.getenv(DB_PASSWORD_KEY); if (key null || key.isEmpty()) { throw new IllegalStateException(环境变量 DB_PASSWORD_KEY 未配置无法解密数据库密码); } try { return AesUtil.decrypt(inner, key); } catch (Exception e) { throw new IllegalStateException(数据库密码解密失败请检查 DB_PASSWORD_KEY 是否正确, e); } } // 非 ENC 开头的配置直接原样返回方便本地开发环境调试 return encryptedValue; } }resolvePassword这个方法值得注意两点。第一它优先验证环境变量是否存在避免密钥缺失时报出含糊的数据库连接错误排查起来麻烦第二我们保留了明文直通的逻辑本地开发时不需要配环境变量直接写明文密码即可但生产环境必须用ENC()密文通过启动参数强约束。4.4 为什么我前后对比后选了这条路径对比官方 RSA 方案和自定义 AES 方案最后的结果是这样的对比维度Druid 官方 ConfigTools自定义 AES 环境变量加密算法RSA 非对称AES/CBC 对称密钥存放公钥在配置私钥在生成时出现密钥只在环境变量密码变更需重新生成密钥对和密文只需重算密文密钥不变仓库泄露风险密文 公钥理论安全只有密文必须再拿服务器权限本地开发体验需要维护密钥对明文直通 密文可切运维维护成本中低我最终选了自定义 AES[仅作参考以下是符合所有要求的结构化博文正文。4. 自定义数据库密码加密方案为什么我用 AES 而不是继续扩展 RSA](widget)4.1 整体设计思路我最终采用的结构是 AES 对称加密 环境变量盐/密钥。核心逻辑是配置文件里写密文密钥放在操作系统环境变量中Spring 启动时在数据源构建阶段完成解密。这个设计把安全边界从仓库不被完整拖走升级到了即使仓库被拖走攻击者也拿不到环境变量。收效是可被感知的。可能会有朋友问Druid 官方方案中的 RSA 也是非对称加密为什么还要换成 AES原因是实际落地场景不一样。RSA 的密钥对生成、分发、销毁流程对团队协作来说很重多人开发时私钥总会在某个环节出现在某个人本地。AES 对称加密只有一把钥匙存在服务器环境变量里代码仓库里永远不需要出现密钥本体运维心智负担小很多。整体流程拆开来看用独立工具类生成一个 AES 密钥开发/运维各执行一次Base64 编码存入服务器环境变量用该密钥对数据库明文密码进行加密得到密文将密文填进application.yml带ENC(...)标识前缀Spring Boot 启动时Druid 配置类读取环境变量里的密钥对ENC(...)做解密解密得到的原文只存在于当前 JVM 内存中用于创建数据库连接池这套流程第一次在本地跑通后你会有一种很安心的感觉随便打开配置文件看不到任何和真实密码有关的字符。4.2 密钥和密文的生成过程先写一个独立的 Java 工具类或者你也可以直接用命令行工具实现 AES 加解密。这里我用 Java 原生的Cipher类来实现import javax.crypto.Cipher; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import java.util.Base64; public class AesUtil { private static final String AES AES/CBC/PKCS5Padding; public static String encrypt(String original, String base64Key) throws Exception { byte[] keyBytes Base64.getDecoder().decode(base64Key); SecretKeySpec keySpec new SecretKeySpec(keyBytes, AES); Cipher cipher Cipher.getInstance(AES); // 使用固定的 IV或者从密文头读取这里演示固定 IV 偏移 byte[] iv 0123456789abcdef.getBytes(); IvParameterSpec ivSpec new IvParameterSpec(iv); cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec); byte[] encrypted cipher.doFinal(original.getBytes(UTF-8)); return Base64.getEncoder().encodeToString(encrypted); } public static String decrypt(String encryptedBase64, String base64Key) throws Exception { byte[] keyBytes Base64.getDecoder().decode(base64Key); SecretKeySpec keySpec new SecretKeySpec(keyBytes, AES); Cipher cipher Cipher.getInstance(AES); byte[] iv 0123456789abcdef.getBytes(); IvParameterSpec ivSpec new IvParameterSpec(iv); cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec); byte[] decoded Base64.getDecoder().decode(encryptedBase64); byte[] decrypted cipher.doFinal(decoded); return new String(decrypted, UTF-8); } }上面是加密工具类。特别注意 IV 的使用前端接口加密、数据库密码加密这类场景IV 可以是固定值但你得知道它不能随便改。安全要求更高时可以随机生成 IV 并拼在密文前面解密时再截取这段可以自己按需扩展。执行加密时用一个单独的测试类或者命令行先设置密钥export DB_PASSWORD_KEYK7gH2nQm9vX4pLzR8tWcY6sB3dF0jA然后调用加密方法public class EncryptMain { public static void main(String[] args) throws Exception { String original YourRealDbPassword123; String encrypted AesUtil.encrypt(original, System.getenv(DB_PASSWORD_KEY)); System.out.println(encrypted); } }输出的字符串就是我们要的新密码。它的特征是没有字母大小写规律、长度固定、每次加密因为 IV 相同结果相同方便你核对配置是否复制对。有人会问是不是每次加密结果都一样对因为我写了固定 IV。如果你希望每次都不同那加密结果每次都会变但解密时必须能从密文里取回 IV简单做法是把 IV 放在密文前几个字节。我这边为了让运维方便比对配置选择了固定 IV 的方式敏感度不高的内部系统可以接受如果是外网环境建议升级为随机 IV。4.3 为什么我前端不选 base64 二次混淆实现过程中有一种看起来更复杂的常见做法base64 套 URL 编码再套一层 AES。说实话没必要。AES 密文本质就是一个字节数组Base64 编码已经足够用于存储在配置文件中。你叠多少层编码都只是让日志输出更乱对安全增益为零。复杂度大了反而容易在解密时报编码错误。很多安全方案失效不是因为算法不够强而是因为密钥管理混乱。我见过有的项目AES 密钥和密文放在同一个 yml 文件里加解密全做了但等于没做。所以密钥永远不要跟密文同仓库。哪怕你再写一个application-secret.yml并且把它加进.gitignore都要好过放在公共配置里。4.4 环境变量读取与解密的时序问题另一个踩过的坑是Spring Boot 加载配置的顺序非常早在DataSource创建之前Druid 就必须拿到真实密码。所以解密不能等到启动完成后做必须在Configuration类加载时同步完成。我把解密封装在一个Component里Component public class DruidPasswordDecryptor { private final String decryptedPassword; public DruidPasswordDecryptor() { String key System.getenv(DB_PASSWORD_KEY); if (key null || key.isEmpty()) { throw new IllegalStateException(环境变量 DB_PASSWORD_KEY 未配置无法解密数据库密码); } String encrypted ENC(这里放配置里的密文); this.decryptedPassword decrypt(encrypted, key); } public String getDecryptedPassword() { return decryptedPassword; } }然后 Druid 数据源构造时直接调用decryptor.getDecryptedPassword()。这里有个细节如果直接在构造器里写死获取配置Spring 的注入顺序容易发生Bean尚未初始化的问题。最好是把Value(${spring.datasource.password})注入到属性中再在PostConstruct或构造方法里完成解密。更稳妥的方式是用一个专门的DruidDataSourceFactory把加密密码和环境变量都作为入参传入返回一个已经配置好真实密码的数据源实例。不要在Configuration里做太重的逻辑。5. 完整集成实践从依赖引入到连接池可用5.1 Maven 依赖与版本选择集成第一步确认 Druid 依赖是否存在。Spring Boot 2.7.x 项目我使用的是dependency groupIdcom.alibaba/groupId artifactIddruid-spring-boot-starter/artifactId version1.2.20/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version scoperuntime/scope /dependency有个版本坑要说明不要用druid老版本 1.1.x 的 starter它的自动配置和 Spring Boot 2.3 的配置绑定机制有兼容问题具体表现是某些druid.开头的自定义属性不生效。1.2.20 是 2024 年较稳的版本后续更高版本建议在测试环境验证过再升。如果你使用的不是 MySQL 而是 PostgreSQLmysql-connector 那个依赖换成对应的 PG 驱动即可Druid 本身的配置方式不变。密码加密这部分不受数据库类型影响。5.2 application.yml 的完整配置这里直接给出一份可跑的 yml 配置注解写在注释里spring: datasource: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/your_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse username: your_user password: ENC(l4nsC4mM5t3kKfY6qxZ9dNv2gP4rNb3xQ) druid: # 初始连接数 initial-size: 5 # 最小空闲连接数 min-idle: 5 # 最大活跃连接数 max-active: 20 # 获取连接最大等待时间 max-wait: 60000 # 检测连接有效的SQL validation-query: SELECT 1 FROM DUAL test-while-idle: true test-on-borrow: false test-on-return: false # 开启监控统计 web-stat-filter: enabled: true stat-view-servlet: enabled: true login-username: admin login-password: ENC(这里可以也做加密) # 公共配置段 hikari: # 这里不配置因为我们要用druid保留默认值即可以免冲突注意一点如果你的项目里同时有spring.datasource.hikari等旧配置要先删掉否则 Spring Boot 自动配置会尝试创建 Hikari 相关bean引起冲突。这是我实际集成时遇到的第一个报错看到控制台出现Failed to configure a DataSource: url attribute is not specified时往往不是缺 url而是配置段冲突。5.3 数据源配置类与解密注入如果使用了 druid-spring-boot-starter大部分配置可以由 yml 直接绑定到 DruidDataSource但解密逻辑需要手动介入。推荐写法是定义一个Configuration类继承DruidAbstractDataSource或在初始化阶段替换密码字段。我这里用一个静态内部配置类避免参数注入时序乱掉Configuration public class DruidDataSourceConfig { Value(${spring.datasource.url}) private String url; Value(${spring.datasource.username}) private String username; Value(${spring.datasource.password}) private String encryptedPassword; Value(${spring.datasource.driver-class-name}) private String driverClassName; Bean Primary public DataSource dataSource(DruidPasswordDecryptor decryptor) { DruidDataSource dataSource new DruidDataSource(); dataSource.setUrl(url); dataSource.setUsername(username); dataSource.setPassword(decryptor.getDecryptedPassword()); dataSource.setDriverClassName(driverClassName); dataSource.setInitialSize(5); dataSource.setMinIdle(5); dataSource.setMaxActive(20); dataSource.setMaxWait(60000); dataSource.setValidationQuery(SELECT 1 FROM DUAL); dataSource.setTestWhileIdle(true); dataSource.setTestOnBorrow(false); dataSource.setTestOnReturn(false); return dataSource; } }Bean Primary这句非常关键。因为 Spring Boot 自动配置会默认注册一个DataSource如果不覆盖注入DataSource的地方拿到的是 HikariCP而不是 Druid。通过Primary把我们的 Druid bean 设为首选并且在 yml 中显式写出spring.datasource.typecom.alibaba.druid.pool.DruidDataSource双保险。提示如果你还要启用 Druid 监控页面需要在 application.yml 里配druid.stat-view-servlet.enabledtrue并设置登录账号密码。账号密码这一处也可以使用 AES 加密复用同一个解密组件即可。5.4 运行起来之后从连接日志验证加密是否生效集成完之后判断加解密是否生效最直接的方式是看启动日志和连接建立状态。正常情况启动时日志中不会打印真实密码。我遇到过一种情况是某些版本的 MySQL 驱动或连接池会在 DEBUG 日志级别输出连接 URL其中包含密码参数。建议在logging.level中把涉及数据库驱动的日志级别控制在 INFOlogging: level: com.mysql: WARN com.alibaba.druid: INFO org.springframework.boot.autoconfigure.jdbc: INFO另一个验证手段是故意把密钥配错观察启动是否快速报错。如果错误信息是无法解密说明先走到了我们的解密逻辑加密链路没有问题。如果错误是数据库拒绝连接那说明真实密码已经被传给了驱动但密码本身错误。两种错误的排查方向完全不同。我实际验证过的正常结果是这样应用启动成功 → Druid 初始化连接成功 → 业务接口查询正常 → Druid 监控页面能看到活跃连接。全程不会在任何一个日志文件中出现明文密码。6. 集成过程中的坑逐条排查记录6.1 Spring Boot 自动配置和 Druid 的配置冲突这个坑最隐蔽也最容易误导排查方向。我最初只引入了druid-spring-boot-starter然后在 yml 中简单写了spring.datasource.type...结果启动时控制台直接报错Failed to configure a DataSource: url attribute is not specified and no embedded datasource could be configured.明明配置了 url为什么说没有排查后发现问题出在借用了从未删除的 Hikari 配置项。Spring Boot 的DataSourceAutoConfiguration会先尝试用spring.datasource.hikari.*绑定到 Hikari 的属性如果相关前缀存在它会继续创建 Hikari 实例但此时 Druid starter 已经注册了DruidDataSource两个存在竞争关系。解决办法很直接一把druid-spring-boot-starter之外的数据源自动配置排除掉二显式定义自己的DataSourcebean。排第一的做法一行代码可以搞定SpringBootApplication(exclude DataSourceAutoConfiguration.class)但如果你还需要 Druid 监控统计自动配置不建议把整个DataSourceAutoConfigurationexclude 掉。更好的方案是删掉所有spring.datasource.hikari开头的配置段只保留druid段。6.2Value注入的字符串被环境变量覆盖这个坑比较阴。我有一个同事在某台服务器上把数据库密码写死进了系统环境变量SPRING_DATASOURCE_PASSWORD然后我们在配置里明明写的是密文ENC(...)但最终 Druid 拿到的是服务器的明文环境变量导致解密逻辑绕过去了配置形同虚设。Spring Boot 的配置优先级中环境变量高于 yml 文件。只要系统存在SPRING_DATASOURCE_PASSWORD这种 env 映射过的变量它就会被用于覆盖原有spring.datasource.password。而我在DruidDataSourceConfig中通过Value(${spring.datasource.password})注入时拿到的是覆盖后的明文ENC(...)前缀判断会直接跳过。这个问题排查花了我不少时间因为本地环境没有这个变量发布到生产才出现崩溃。修复方案有两个清理服务器上残留的SPRING_DATASOURCE_PASSWORD环境变量统一使用自定义的DB_PASSWORD_KEY或者修改解密器逻辑对任何非空密码都先尝试解密解密失败再放行明文不再依赖ENC()前缀判断我最终采用第二种因为更健壮。但要注意这样做的风险是如果密码本身不是我们的密文格式解密必定抛异常。所以在decrypt方法里加一个try/catch失败时返回原值并打一条 WARN 日志。6.3 多环境配置下密钥不统一的麻烦开发、测试、生产三套环境各自生成了不同的 AES 密钥这是规范问题。有一次测试环境连接失败排查了半天最后发现是测试服务器的环境变量DB_PASSWORD_KEY误用了生产密钥而 yml 里却是测试环境生成的密文。两边驴头不对马嘴。解决规范问题采用两套机制密钥统一由核心配置管理发布时由运维从统一的密钥管理系统拉取并设置环境变量开发环境本地可以用自己的任意密钥预先在启动脚本中校验环境变量是否为合法 Base64 且长度正确避免启动到一半才报解密失败示例启动脚本if [ -z $DB_PASSWORD_KEY ]; then echo [ERROR] DB_PASSWORD_KEY is not set exit 1 fi echo DB_PASSWORD_KEY is set, length${#DB_PASSWORD_KEY}这里不要打印完整的密钥内容只打印长度避免泄露。6.4 Druid 监控页面的登录密码也要防泄露很多人处理了spring.datasource.password却遗漏了 Druid 监控页面的login-password。这个密码往往明文写在 yml 里。其实监控页面的登录密码也是敏感信息攻击者拿到后可以直接登录 Druid 控制台查看 SQL 执行统计甚至通过某些配置操作干扰数据源。建议监控页面密码也走同一套 AES 加解密。如果嫌麻烦最低限度是用环境变量注入spring: datasource: druid: stat-view-servlet: login-username: ${DRUID_MONITOR_USER} login-password: ${DRUID_MONITOR_PASSWORD}这样至少不会出现在代码仓库里。要更安全就复用刚才的DruidPasswordDecryptor把监控相关配置也做成加密字段。6.5 密码变更流程容易忽略的关联地方数据库本身密码修改后需要同步更新的不止一处。我整理过一份清单application.yml中的ENV(...)密文需要重新生成服务器环境变量DB_PASSWORD_KEY如果被修改过需要用新密钥重新加密所有密码被 Druid 监控页面的login-password如果你也做了加密同步重生成任何在代码中直接Value(${spring.datasource.password})用到明文的第三方库都要检查有一次我只改了 yml 里的密文忘了监控页面的密码结果监控页面一直登录不了排查了半天才发现是两个密码不一致。7. 把密码安全这件事再做深一步后续可扩展的方向7.1 从配置中心拉取密钥而不是用系统环境变量环境变量方案解决了仓库泄露问题但还没解决多节点统一运维的问题。如果服务部署在十台机器上你需要在每台机器都设置DB_PASSWORD_KEY一旦密钥轮换要连十台机器。这个场景下可以让密钥从配置中心拉取比如使用业界常见的配置中心组件把密钥作为配置项发布应用启动时动态拉取。但有一个新的问题如果配置中心本身被攻破密钥就跟着泄露。所以更完善的设计是配置中心存密文密钥只存在启动时输入的一次性口令中或者使用 KMS密钥管理服务让解密在硬件/云服务内部完成程序本身接触不到密钥明文。这个属于进阶层中小项目暂时不需要。7.2 定期轮换密钥的自动化密码越久不换泄露风险越高。建议至少每三个月轮换一次数据库密码。轮换涉及的操作步骤修改数据库中的真实密码用旧密钥或新密钥重新生成密文建议换新密钥更新服务器环境变量DB_PASSWORD_KEY更新应用配置文件中的ENC(...)密文重启应用观察连接日志确认监控页面可以正常登录如果想自动化可以把这些步骤写进 CI/CD 的发布流程中替换的过程用脚本完成。但注意脚本日志不能打印密钥和密码。我在脚本中就吃过亏用了set -x展开调试结果密钥被推进了日志平台立刻轮换了一遍。7.3 从数据源层面再加一道防护Druid SQL 黑名单密码加密解决的是拿到配置文件无法连接数据库这个问题。但如果密码已经通过正规业务途径进入数据库甚至某次代码逻辑把密码展示在前端页面那加密就失去意义了。所以建议配合 Druid 的 SQL 防火墙功能配置禁止删除表、禁止批量更新等规则。spring: datasource: druid: filter: wall: enabled: true config: delete-allow: false update-where-none-check: true multi-statement-allow: false这套配置的核心价值即使密码泄漏给外部运维人员也无法从应用层执行危险 SQL。相当于把数据库的最后一道防线从密码变成了策略。7.4 审计意识比技术手段更重要我越来越觉得数据库密码安全本质上是流程问题代码只是工具。最终让密码真正安全的是密码不出现在代码仓库密码不出现在日志平台密码不出现在接口返回密码在手写草稿纸上用完即焚密码轮换有固定流程和负责人技术方案做得再好只要有一个环节习惯不好比如把密码贴在显示器上或者发到群里艾特运维前面的一切努力就可能白费。所以我在团队内部推行了这么几条习惯任何数据库操作优先走内网跳板段落式密码不用root这样的公共账号开发环境密码使用独立低权限账号生产环境密码只有核心运维和架构师权限可读。8. 最终总结配置文件中再也不会出现真实密码把整条链路做完整之后最终效果是application.yml中只有一串无意义的密文没有真实密码真实密码只有服务器上的环境变量能参与解密环境变量不在 Git 仓库中启动时解密只在内存中完成日志和监控不会泄漏原始密码即使 Git 仓库整体泄露攻击者拿到的只是密文无法直接使用配合 SQL 防火墙即使拿到密码也无法从业务接口做危险操作这套方案运行了多个项目之后验证稳定。我恢复了几次数据库连接每次都能正常解密切换换了两次数据库密码按照流程更新密文和密钥后应用短时间重启即可恢复。若你的项目受团队规范限制暂时不能使用密钥管理服务先按这篇文章的 AES 环境变量方案落地是完全可行的。它的核心价值就是花很少的改造成本彻底消灭数据库密码明文出现在配置文件这个最常见的隐患。如果你在集成 Druid 时遇到别的奇怪问题比如监控页面不显示、连接池创建失败、密码解密后带不可见字符欢迎在评论区描述一下你用的 Spring Boot 版本和 Druid 版本我看到会逐条回复。