一辆写着password: 123456的 Spring Boot 服务,在发布到生产环境的那一刻,就已经把半个数据库的钥匙挂在了墙上。先别急着做性能优化,也别一头扎进微服务改造,把配置文件里的数据库密码、Redis 密码、接口密钥这类东西加密一遍,才是高级工程师该先动手的事情。
我这次分享的是 Java 开发中绕不开的一个场景:配置文件密码加密。你会看到为什么不能明文放密码、主流的几种加密方案怎么选、若依这类基于 Druid 的框架如何集成解密,以及 Jasypt 这类通用配置加密工具怎么用。还会把密码加密之后容易踩的坑,比如密钥放在哪、轮换怎么做、日志里会不会泄露,一并讲透。适合正在做 Spring Boot 或若依项目、被安全扫描或领导要求整改的同学参考。
1. 为什么配置文件里的数据库密码不能明文暴露
1.1 明文密码最常见的几种出逃路径
很多项目其实不是被攻击才泄露密码的,大多数时候是自己“送”出去的。最常见的是代码仓库泄露:application.yml里有明文数据库密码,一个疏忽把仓库设置成公开,或者被某一个拥有访问权的离职人员拖走,密码就跟着代码一起出去了。更隐蔽的是 Git 历史记录,哪怕后来有人把密码从文件里删掉重新提交,之前的那条记录仍然留在.git目录里,任何人 clone 全部分支和历史 commit 都能看到。
另一个路径是日志和异常信息。Java 服务启动时如果数据源配置出错,Spring 会把 DataSource URL、用户名、密码一整套打印到启动日志里。有的团队习惯把日志统一采集到 ELK 或云日志平台,权限管理不严格,运维、测试甚至前端同学都能搜到关键词,密码就这样出现在检索结果里。
还有一类是配置本身权限混乱。服务器上配置文件是 644 权限,任何登录用户都能读;或者配置中心没有做细粒度授权,比密码泄露更早的是所有人都能浏览所有配置。这类问题在传统运维风格的团队里非常常见。
1.2 高级工程师对“敏感信息”的定义
高级工程师处理配置信息时,核心原则可以概括为三条:最小暴露、纵深防御、密钥与配置分离。数据库密码这类凭证信息,不应该以可读形式出现在任何能被多人共享的文件中;即使测试环境数据库没有重要数据,也不建议明文存放,因为攻击者一旦拿到测试环境的访问权,很可能以此为跳板去探测生产环境,这其实是一条很现实的攻击路径。
我见过不少团队认为“内网环境无所谓”“测试环境无所谓”,等到安全扫描报告出来,要求整改配置文件中所有敏感字段,才手忙脚乱地翻项目。与其这样,不如从项目第一天就把加密能力做进去。这不会花太多时间,却能在检查时拿出一个像样的答案。
2. 配置文件密码加密的主流方案,各自适合什么场景
2.1 四种方案一句话讲清楚
第一个是 Druid 自带的连接配置解密功能。Druid 提供了ConfigTools,可以生成非对称密钥对,用私钥对数据库密码加密,配置文件中保存公钥和密文,Druid 在创建数据源时自动用公钥解出真实密码。这个方案对使用 Druid 连接池的项目来说是最轻量的,若依框架就是典型场景。
第二个是 Jasypt Spring Boot Starter。它是一个更通用的配置加密方案,不关心配置项的类型,可以通过一个约定的密文格式ENC(密文)加密任意字符串。数据库密码、Redis 密码、调用第三方接口的 Key、SMTP 密码都可以统一处理,对项目的侵入性也很小,是很多 Spring Boot 项目做配置加密的首选。
第三个是环境变量注入。把密码放在环境变量中,配置文件里写${DB_PASSWORD},部署时由容器或运维注入真实值。严格来说这不算“加密”,但密钥不再存在于代码和配置文件中了,安全性提升很大。缺点是环境变量多了之后管理起来麻烦,而且只要登录到服务器执行env,密码还是能看到。
第四个是外部配置中心加 KMS。把配置放到 Nacos、Apollo 之类的配置中心,再用云厂商的 KMS 或自建 HS 服务做密钥管理,配置中心存储密文,应用运行时通过密钥服务动态解密。这是微服务架构下比较完善的做法,但引入的组件和运维成本也明显更高,适合对安全要求很高、团队规模较大的场景。
2.2 方案对比表
| 方案 | 核心原理 | 优点 | 缺点 | 适合场景 |
|---|---|---|---|---|
| Druid ConfigTools | 非对称加密,私钥加密,公钥解密 | 集成简单,若依等框架可直接用 | 只适用于 Druid 数据源密码 | 单体应用、若依项目 |
| Jasypt | 对称加密,统一的 ENC 密文格式 | 通用性强,可覆盖所有配置项 | 加解密密钥为同一个,需严加保护 | Spring Boot 任意项目 |
| 环境变量 | 配置文件只留变量引用 | 简单直接,无额外依赖 | 密码仍以明文存在于运行环境中 | 小型项目、容器部署 |
| 配置中心 + KMS | 密文存储在配置中心,KMS 管理密钥 | 密钥集中管理,支持审计和轮换 | 引入组件多,运维成本高 | 微服务、大规模集群 |
2.3 我的选型建议
我在实际项目里一般这样判断:如果项目已经使用了 Druid,且只有数据库密码需要加密,优先采用 Druid 自带的 ConfigTools 解密,改动最小;如果敏感配置项不止数据库密码,或者希望以后统一管理所有配置中的敏感字段,直接上 Jasypt 更省心;如果部署环境是 Kubernetes,且密钥已经有严格的环境变量管理,环境变量也能接受,但前提是日志和监控里要做好脱敏。
还有一个容易被忽略的点:方案选型要考虑团队后续的运维能力。配置中心加 KMS 看起来很正规,但如果团队没人熟悉密钥轮换流程,反而容易在需要改密时手忙脚乱。先做一个能坚持用下去的方案,比做一个看起来高大上但维护不了方案更务实。
3. 实操第一步:用 Druid 自带 Filter 做数据库密码解密(若依项目常用)
3.1 Druid 的数据库密码解密到底做了什么
Druid 的ConfigTools本质上是一个基于 RSA 算法的加解密工具。它生成一对密钥:私钥用于加密,公钥用于解密。为什么是“私钥加密、公钥解密”?因为私钥只掌握在开发者手里,用来生成密文;公钥随项目部署到服务器,Druid 只需要公钥就能在运行时解出密码,私钥不必出现在生产环境里,这就是它比对称加密更适合数据源密码的原因——即使生产配置文件泄露,攻击者拿到公钥也无法反推明文密码。
在 Spring Boot 中接入时,核心思路是利用 Druid 提供的ConfigFilter。配置完成后,Druid 在初始化数据源时读取config.decrypt=true和config.decrypt.key,然后调用ConfigTools.decrypt(publicKey, password)解出实际密码。整个过程对业务代码是透明的,你不需要自己写解密逻辑。
3.2 手把手走一遍:从生成密文到启动验证
第一步,用命令行生成密钥和密文。假设你的项目用的是 Druid 1.2.20,先确认本地有 druid 的 jar 包,然后执行:
java -cp druid-1.2.20.jar com.alibaba.druid.filter.config.ConfigTools "你的数据库密码"命令执行后会输出三样东西:privateKey、publicKey、password。其中password就是密文,后面要用到。注意不要在服务器上重复执行生成,密钥对要保持稳定,否则已经加密过的密码会因为密钥不同而解密失败。
第二步,把公钥和密文写进配置文件。在application.yml中,你原来的数据源配置可能是这样的:
spring: datasource: url: jdbc:mysql://localhost:3306/demo username: root password: 123456改成加密方式后,需要额外开启 Druid 的 ConfigFilter,并指定公钥和密文:
spring: datasource: url: jdbc:mysql://localhost:3306/demo username: root password: "这里填 ConfigTools 生成的密文" druid: filter: config: enabled: true connection-properties: "config.decrypt=true;config.decrypt.key=这里填 publicKey"这里有一个值得注意的细节:password字段填的是密文,不是明文,而且我在密文外层加了双引号。这是因为 RSA 加密后的密文/base64 字符串可能包含特殊字符,YAML 不加引号时有概率解析异常。很多同学改完配置后报错,一排查发现不是解密逻辑问题,而是 YAML 语法对特殊字符处理不严谨。
第三步,启动服务。如果能在日志里看到数据源初始化成功,就说明解密生效了。我习惯在启动后做一个最小查询验证,比如写一个简单的 Controller 查询数据库版本,确保连接池中的连接可用。
3.3 如果碰到“密码无法解密”怎么排查
最常见的问题有三个:公钥和密文没有配对,把privateKey当成publicKey填进去了,或者中间多复制了一个空格;配置项没有生效,filter.config.enabled这种配置在较老的 Druid 版本里字段会有差异,需要确认你用的是否有druid前缀;YAML 解析问题,密文被截断,尤其是密文里包含/、+、=这类字符时,必须加引号。
还有一个陷阱:若依框架有些版本在DruidConfig中手动创建了DruidDataSource,这时候application.yml里的spring.datasource.druid.*不一定完全覆盖,你需要在DruidConfig中把 ConfigFilter 的decrypt属性和公钥手动 set 进去,或者在配置类中读取配置文件注入。这种情况建议先打印一下最终构造出来的 DataSource 对象,确认password到底是否被解密。
4. 实操第二步:用 Jasypt 统一加密任意敏感配置
4.1 Jasypt 能解决什么项目痛点
Druid 的方案好用的前提是你已经选定了 Druid 连接池,而且需要加密的确实只有数据库密码。但实际项目中,配置文件里往往还有 Redis 密码、消息队列用户名密码、第三方接口的 Token、SMTP 授权码等等。每一个都单独做一套加密逻辑,代码会变得很碎。
Jasypt 解决的是“统一加密规范”这件事。它的核心机制是把敏感配置的值用一种约定的格式包起来,例如ENC(密文)。Spring Boot 启动时,Jasypt 的处理器会扫描配置源,遇到ENC(...)包裹的值就调用解密器解密,然后把真实值交给应用程序。开发者不需要为每种配置写额外的解密方法,只需要在配置文件中把明文值替换成ENC(密文)。
4.2 Spring Boot 快速接入 Jasypt
引入依赖:
<dependency> <groupId>com.github.ulisesbocchio</groupId> <artifactId>jasypt-spring-boot-starter</artifactId> <version>3.0.5</version> </dependency>然后在application.yml中指定加密算法和密钥。注意密钥不要直接写在这里,而是通过环境变量引用:
jasypt: encryptor: algorithm: PBEWITHHMACSHA512ANDAES_256 password: ${JASYPT_ENCRYPTOR_PASSWORD}接下来需要生成密文。最简单的方式是写一个一次性的 Java 类,或者直接用官方提供的加密工具。我用的是在 Spring Boot 的 main 方法里临时注入的 Bean:
@SpringBootApplication public class EncryptUtil { public static void main(String[] args) { StringEncryptor encryptor = new DefaultLazyEncryptor( new StandardPBEByteEncryptor()); String plainText = "你的数据库密码"; String cipherText = encryptor.encrypt(plainText); System.out.println(cipherText); } }真实项目中,我更推荐用 Maven 插件生成密文:
mvn jasypt:encrypt-value -Djasypt.encryptor.password=你的密钥 -Djasypt.plugin.value="你的密码"生成的结果是一串加密后的字符,形如ENC(abc123...)。把它放回原来的配置项,例如:
spring: datasource: password: ENC(密文) redis: password: ENC(密文)Jasypt 有一个自动识别机制,所有注入到 Spring Environment 中的属性值,只要符合ENC(...)格式,都会被自动解密。这意味着不仅application.yml里能用,非配置文件中的动态配置也能被统一处理。
4.3 密钥放哪:别把它和密文放一起
Jasypt 是对称加密,加密和解密用的是同一个密钥。如果你把密钥写进application.yml,那就等于把保险柜钥匙贴在保险柜门上,加密也基本失去了意义。我在本地调试时会把密钥放在~/.bashrc里,部署到服务器时通过环境变量传入,或者放到 K8s 的 Secret 中。
启动命令的方式可以参考:
export JASYPT_ENCRYPTOR_PASSWORD=$(cat /etc/secret/jasypt_key) java -jar your-app.jar这里有一个容易踩的坑:环境变量一旦变更,所有用它加密过的密文都会失效,启动时直接报解密错误。所以密钥的生命周期管理要提前想清楚,不要随便更换。
4.4 Jasypt 避坑要点
我遇到过的最典型的坑是加密串在日志里被原样打印出来。如果数据库连接失败,异常信息里会把包含ENC(...)的配置值完整输出,虽然比明文要好一些,但对攻击者来说已经有了明确的解密目标。更稳妥的做法是在 Logback 配置里设置脱敏,或者自定义PatternLayout,将类似于ENC(...)的片段替换为******。
另外,如果同一套代码要部署到多个环境,不同环境建议使用不同的 Jasypt 密钥。密文是不能跨密钥用的,比如测试环境生成的密文放到生产环境,会因为密钥不同而解密失败。我见过有同事把测试环境密文原封不动复制到生产环境,导致生产环境启动失败,这个错误很常见,必须特别注意。
5. 高级工程师的密钥管理实战:轮换、审计与日志脱敏
5.1 密钥本身怎么保护
做配置加密最忌讳的是“只加密了密码,没保护密钥”。Jasypt 的密钥如果写在/etc/profile或启动脚本里,并且脚本文件权限是 644,那和你之前把明文密码放在配置文件里没本质区别。我建议遵循“密钥颗粒度最小、权限最小”的原则:能放在单个应用私有的本地文件中,就不要放到公共配置文件里;能通过 K8s Secret 注入,就不要写进启动脚本。
对于本地密钥文件,权限一定要收紧:
chmod 600 /etc/secret/jasypt_key sudo chown appuser:appuser /etc/secret/jasypt_key如果你使用的是云厂商的 KMS,由 KMS 负责真密钥的保存和解密,应用运行时通过 SDK 向 KMS 请求解密结果,这种方式更安全,但需要提前规划好权限策略,比如限定某个 IAM 用户只能访问某一个密钥。
5.2 密钥轮换要做滚动兼容
密钥轮换是一个“说起来容易,做起来惊吓”的事情。如果直接换掉密钥,所有历史密文都会失效,然后就需要停机改配置,停得越久风险越大。更稳妥的做法是采用“双密钥并行过渡”的方式。比如 Jasypt,自定义一个StringEncryptor,解密时先尝试当前密钥ENC(新密文),如果失败再尝试旧密钥ENC(旧密文),解密成功后把结果返回;业务运行期间,逐步把旧密文更新为新密文,最后移除旧密钥。
这个方案写起来也不复杂,核心是一个多密钥轮询的逻辑。我每次做这类改造都会加一条审计日志:识别到某个配置仍然在使用旧密钥解密时,输出一条 WARN,提示运维尽快替换,这样轮换进度就很清晰了。
5.3 日志和监控里的密码脱敏
在配置加密完成之后,下一个对手就是日志系统。Spring 启动时输出的属性值、异常信息、sql 监控页面、甚至一些热更新工具打印的配置快照,都可能把解密后的明文密码带出来。我的经验是部署后先跑一轮“日志审查”。用 grep 搜索关键词:
grep -r "password\|secret\|token" /var/log/app/*.log如果发现了明文,就需要对日志框架做脱敏。以 Logback 为例,可以自定义一个PatternLayout,通过正则把密码字段的值替换为掩码。更简单一点的方式是统一封装DataSourceProperties,在日志输出时做掩码,但这要求团队约定好不直接打印配置对象。
5.4 审计:谁在什么时候改了配置
对高级工程师来说,配置加密只是第一步,可审计性同样重要。企业级项目至少要能回答“这份配置是什么时候被改的,谁改的,之前的值是什么”。如果使用 Git 仓库管理配置文件,可以直接通过提交历史和权限来追查;如果使用配置中心,建议开启变更记录和操作人字段。还有一种情况是配置文件不在 Git 里,比如服务器本地的外部化配置,那就至少要在变更前备份,并记录变更时间。
我不能提供一个万能的审计方案,但有一条很实际的建议:配置变更通知一定要接入告警。无论是数据源密码、密钥文件,还是连接池参数,只要被修改就立刻给负责人发通知,很多人都是靠这个早发现了潜在问题。
6. 常见问题与排查技巧实录
6.1 Jasypt 和 Druid 一起用时解密顺序问题
有些项目既用 Druid,又用 Jasypt,就会出现一个连接配置的先后顺序问题。如果你用 Jasypt 对 Druid 的公钥或者数据库密文做二次包装,例如写成config.decrypt.key: ENC(公钥),那么必须确保 Jasypt 在 Druid 创建数据源之前已经完成解密,否则 Druid 拿到的还是一个ENC(...)字符串,无法正确解密密码。
解决思路有两条:一是只对最终密码用 Jasypt,公钥仍用明文环境变量注入;第二条是自定义一个EnvironmentPostProcessor,在 Environment 准备阶段提前把 Jasypt 解密逻辑执行完,这样后续所有 Bean 看到的都是解密后的值。第二种办法比较“高级”,但逻辑要自己控制好,不能和 Spring Boot 默认的配置加载顺序冲突。
6.2 配置文件加载顺序导致的空密码问题
Spring Boot 的配置加载顺序是:命令行参数、Java 系统属性、系统环境变量、application.properties、application.yml。如果加密配置项依赖一个环境变量,但这个变量在启动脚本中没有正确 export,应用启动时不会直接报错,而是当作没有设置该值,可能把空值带入密码。这个问题的排查耗时很长,因为有时并不是环境变量不存在,而是启动脚本里有特殊字符导致变量没有生效。
我建议启动时打印关键配置值是否存在,但不要打印真实值,比如:
log.info("Jasypt password configured: {}", StringUtils.hasText(System.getenv("JASYPT_ENCRYPTOR_PASSWORD")));这样能在不泄露密钥的前提下,快速确认配置是否被正确读取。
6.3 特殊字符导致 YAML 解析失败
RSA 密文、Jasypt 密文往往是一长串 Base64 字符,里面可能包含/、+、=,偶尔还会出现#、:。如果 YAML 中该配置项没有用引号包围,解析时可能把#当成注释,导致密文被截断,应用启动后解密失败。
好的习惯是:
password: "ENC(8qWQeJvO1...==)"单引号双引号都可以,但要注意如果密文中包含单引号,外层就要用双引号。出现异常时可以先把配置值打印出来对比一下长度,很快就能确认是不是 YAML 解析的问题。
6.4 加密之后启动速度变慢是正常现象吗
解密操作本身非常快,毫秒级别,几乎可以忽略。但如果配置的是 RSA 2048 甚至 4096,并且解密发生在高频率的配置读取路径上,会有一点点开销。我遇到过一个项目反复调用解密方法读取数据库密码,每次请求都走一遍解密,结果连接池频繁创建新连接时耗时明显上升。正确的做法是让解密结果只发生在创建 DataSource 的那一次,后续连接复用,不要每次 getPassword 都解密。
6.5 测试环境也要加密,别拖到上线再说
很多团队觉得测试环境密码不值得加密,反正没有真实数据,而且加密后调试麻烦。但安全扫描往往先扫到测试环境,因为测试环境的访问路径通常比较宽松,配置也可能被更多人看到。我在实际项目中的经验是:让测试环境的加密方式做成可配置的,可以用低强度密钥,但流程必须和生产保持一致。这样上线时的切换成本很低,而且测试环境本身就是演练密钥轮换和风险排查的最佳场所。
如果你正在做类似改造,我的建议是不要追求一步到位。先挑一个最敏感、影响面最小的配置,比如某个模块的调用密钥,用 Jasypt 加密跑通全流程,再逐步替换数据库密码、Redis 密码。等整个流程稳定了之后,再引入密钥轮换和审计机制,很多隐蔽的问题都能在这个过程中暴露出来。最终你会发现,配置文件密码加密这件事,真正难的不是加密逻辑,而是“把密钥和密文分开、把加密后的行为变成团队习惯”。