☰
Spring Boot配置文件密码加密实战:Jasypt与Druid方案详解
2026/9/30 3:04:39 网站建设 项目流程

先说个我自己的经历。去年给团队做代码安全扫描,在 Git 历史里翻出了三个明文数据库密码,其中一个还是生产环境的 root 账号。这事其实不怪开发偷懒,因为从项目第一天起,application.yml里就写着password: 123456,后面所有接手的人都在这个基础上改配置,没人想过要把密码藏起来。直到那次扫描结果贴到群里,大家才开始认真讨论一个问题:Java 配置文件里的密码,到底怎么加密才靠谱?

这篇文章我不想念文档,就结合我用过的几种方案,把“配置文件密码加密”这件事从原理到落地掰开揉碎讲清楚。不管你是刚工作不久想补这块知识,还是在准备 Java 面试想把这个点讲出深度,都应该能从里面挖到点东西。

1. 先搞清楚一个事:配置文件里的密码到底是怎么泄露出去的

很多人的第一反应是“我代码仓库是私有的,不会泄露”。这个想法非常危险。你想想这几个场景:团队里来了新同事,你给他开了仓库权限,他顺手把代码传到自己的公开仓库当备份;某个离职员工手里有历史克隆副本;CI/CD 平台的日志里完整打印了启动命令;还有最容易被忽略的——git log里只要出现过一次明文密码,它就永远留在历史里了,就算你后来改掉也无济于事。

1.1 明文密码真正的风险点不在“文件”,而在“传播链”

密码泄露的路径通常有三条。第一条是代码仓库本身,不管是 Git 还是 SVN,只要账号权限没管好,或者有人误操作推到了公开仓库,密码就跟着代码一起出去了。第二条是日志和监控,Spring Boot 启动时如果开了 debug 级别日志,数据源初始化信息会被完整打出来;有些公司还会把配置文件明文备份到对象存储或者配置中心,这些地方的安全级别未必比代码仓库高。第三条是内网抓包和内存 dump,如果数据库连接串是明文拼出来的,任何一个能登录跳板机的人都可以截获。

1.2 加密的边界:你到底要防谁?

做加密方案之前先想清楚威胁模型。如果你的项目部署在内网,最实际的风险通常来自“代码仓库泄露”和“配置文件被拷贝”,这时候把密码做对称加密,密钥放在服务器环境变量里,就已经能挡住大部分风险。如果还要防内部人员直接看到数据库密码,那就得上更重的方案,比如每次启动时从密钥管理系统拉取密钥,甚至用 Kerberos 做免密认证。我见过最夸张的项目,把数据库密码拆成三段存到三个不同的服务里,启动时还要两两校验,说实话过度设计了。对绝大多数业务系统来说,Jasypt 或者 Druid 自带解密这两个方案已经够用,区别只是你用的连接池和框架是什么。

2. 主流的三种加密落地方式:Jasypt、Druid 解密、自研工具

这块市面上资料很乱,尤其是一堆 Spring Boot 项目混着用,有人用 Druid 自带的解密,有人用 Jasypt 的ENC()前缀,还有人自己写个 AES 工具类在配置里塞一堆看不懂的 Base64。先说结论:如果你用的是 Druid 连接池,直接用 Druid 的ConfigTools解密最顺;如果你用的是 HikariCP 或者别的连接池,而且希望做到“配置文件里所有机密字段都能加密”,那就上 Jasypt。

2.1 三个方案的核心差别对比

方案加密方式配置文件改动适用场景
JasyptENC(密文)占位符,启动时自动解密替换Spring 环境下无侵入,spring-boot-starter-jasypt引入即用通用 Spring Boot/Cloud 项目,不挑连接池
Druid ConfigToolsDruid filter 拦截,connection-properties配置公钥解密只对 Druid 数据源的密码字段生效连接池就是 Druid 的项目,比如若依框架
自研 AES 工具类启动时读取环境变量密钥,手动解密赋值需要自己写代码初始化数据源想完全掌控加解密逻辑,或需要对接第三方加密系统

你可能会问,自研 AES 挺简单的,为什么还要用框架?我过去也这么想,直到维护过一个同事写的“加密工具类”,他把密钥写死在代码里,加密算法用的是AES/ECB/PKCS5Padding,密文不带随机盐,同一个密码加密结果永远一样。这还不如明文,至少明文不会给人“我已经加密了”的错觉。所以后面我给自己定了个规矩:能用成熟方案的绝不手写密码学代码。

2.2 为什么多数项目最后都选了 Jasypt

Jasypt 最大的优势是“无感”。集成之后,你不需要改数据源的初始化逻辑,Spring 容器在读取@Value("${spring.datasource.password}")的时候会自动识别ENC(...)格式的字符串并解密。哪怕配置里有几十个密文字段,比如 Redis 密码、第三方接口密钥、邮件账号,全部可以用同一个机制管理。另一个实际好处是 Jasypt 支持自定义加密器,你可以把国密算法塞进去,也能对接公司内部的 KMS 系统,扩展性比 Druid 的方案灵活得多。

3. Jasypt 集成实操:从引入依赖到启动验证

下面进入正题。我以 Spring Boot 2.7 + Jasypt 3.0.5 为例,讲一遍完整落地过程,顺便把几个容易翻车的地方指出来。

3.1 引入依赖和基础配置

第一步是在pom.xml里加入依赖:

<dependency> <groupId>com.github.ulisesbocchio</groupId> <artifactId>jasypt-spring-boot-starter</artifactId> <version>3.0.5</version> </dependency>

然后修改application.yml:

jasypt: encryptor: password: ${JASYPT_PASSWORD} algorithm: PBEWITHHMACSHA512ANDAES_256 iv-generator-classname: org.jasypt.iv.RandomIvGenerator

注意两个坑。第一,jasypt.encryptor.password尽量不要写在配置文件里,否则加密等于白做,我习惯从环境变量读取。第二,Jasypt 3.x 默认算法变成了PBEWITHHMACSHA512ANDAES_256,不再推荐老的PBEWITHMD5ANDDES,后者是 2.x 时代的默认算法,安全性已经跟不上,而且默认使用固定 IV,同样的明文每次加密结果一样,这本身就是泄露渠道。你如果遇到网上老教程让你配PBEWithMD5AndDES,直接忽略。

3.2 生成密文:两种方式都要会

生成 Jasypt 密文有两种方式,一种是写单元测试调用 API,另一种是直接用 Maven 插件。我推荐前者,因为可以在测试环境里顺便验证解密结果。代码很简单:

@Test void generateEncryptedPassword() { PooledPBEStringEncryptor encryptor = new PooledPBEStringEncryptor(); encryptor.setProviderName("BC"); encryptor.setAlgorithm("PBEWITHHMACSHA512ANDAES_256"); encryptor.setPassword("你的密钥"); encryptor.setIvGenerator(new RandomIvGenerator()); String encrypted = encryptor.encrypt("数据库真实密码"); System.out.println(encrypted); }

这里有个小细节:setProviderName("BC")需要引入 BouncyCastle 依赖,否则部分 JDK 版本会报NoSuchAlgorithmException。如果你的 JDK 版本较新,比如 17+,内置的 SunJCE 也许能直接支持这个算法,但为了保险我还是建议加上 BC。

<dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcprov-jdk18on</artifactId> <version>1.78</version> </dependency>

生成的密文是类似ENC(8gpm8nbHiQ0m6PKgM7HkF0tMfP4qI1mxKmgUdBvABbY=)这样的字符串,直接替换掉原来明文密码的位置。注意ENC()这个外壳是 Jasypt 解密的识别标志,对应配置里的prefix,理论上可以改,但默认配置已经够用,别没事找事。

3.3 启动时怎么传密钥,以及一个反直觉的坑

启动命令这样写:

export JASYPT_PASSWORD=$(cat /etc/secure/jasypt_key) java -jar app.jar

很多人在这一步栽过:直接把密钥放在java -jar命令行参数里,比如-Djasypt.encryptor.password=xxx。这么做的问题是,服务器上执行ps -ef | grep java就能直接看到密钥,等于把钥匙贴在保险柜门上。正确做法是放到环境变量或者受保护的文件里,然后让应用去读。如果担心环境变量也被泄露,那就只能上 KMS 每次启动时动态取密钥,这部分后面讲。

3.4 自定义加密器:把解密过程接到自己的加密服务上

Jasypt 允许你实现StringEncryptor接口,然后注册成一个 Bean。这个功能很适合已经有内部加密平台的公司。我给你一个最小实现思路:

@Component("customStringEncryptor") public class CustomStringEncryptor implements StringEncryptor { @Override public String encrypt(String message) { // 调用内部加密平台,或者用 AES + 随机 IV 实现 return doEncrypt(message); } @Override public String decrypt(String encryptedMessage) { // 对称解密逻辑 return doDecrypt(encryptedMessage); } }

然后配置里指定:

jasypt: encryptor: bean: customStringEncryptor

这里最大的好处是你可以把密钥管理职责完全独立出去。比如让应用启动时先访问一次 KMS 拿到密钥,再用这个密钥初始化自定义加密器,整个过程对业务代码完全透明。我参与过的一个支付系统就是这么做的,研发同学连数据库密码是什么都不知道。

4. 若依框架里的 Druid 数据库密码加密实战

如果你用的不是 Spring Boot 原生配置,而是像若依(RuoYi)这样基于 Druid 连接池的项目,那切入的点就不太一样了。Druid 自己有一套ConfigTools加解密机制,它用的是 RSA 非对称加密,工作流程是:先通过工具生成公钥和私钥,用私钥加密数据库密码,然后把公钥配置在数据源里,Druid filter 在启动时用公钥解出明文密码,再交给连接池。这套机制的好处是私钥可以只掌握在运维手里,出包的配置文件里只有公钥和密文,即使仓库泄露,没有私钥也解不开。

4.1 用 Druid ConfigTools 生成密钥对和密文

去 Maven 仓库找到你项目实际使用的 druid jar 包,然后在本地执行:

java -cp druid-1.2.20.jar com.alibaba.druid.filter.config.ConfigTools "你的数据库密码"

执行完会输出三行东西:privateKey、publicKey、password。其中password就是加密后的密文,privateKey你自己收好,publicKey要配置到数据源里。我在实际操作中遇到过一个问题:如果本地 Java 版本和服务器不一致,生成的密文在服务器上解密失败的概率会增大,所以最稳妥的做法是直接在目标服务器的同款 JRE 环境下生成。

4.2 修改若依的数据源配置

若依默认的数据源配置在application-druid.yml里,你需要做的改动有两处。第一处是url、username保持原样,把password换成生成的密文;第二处是给druid节点加上解密相关的配置。给你一个可直接参考的片段:

spring: datasource: type: com.alibaba.druid.pool.DruidDataSource druid: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/ruoyi?useUnicode=true&characterEncoding=utf8 username: root password: 加密后的密文 filter: config: enabled: true connection-properties: config.decrypt=true;config.decrypt.key=${DRUID_PUBLIC_KEY}

这里面最关键的是config.decrypt.key必须跟ConfigTools输出的publicKey完全一致,注意不要把私钥填进去。若依有些版本里的示例会让你把公钥直接写死在 yml 里,我的习惯是从环境变量读取,因为公钥虽然可以暴露,但配合私钥单独管理,整个安全模型会清晰很多。

4.3 更近一步:Druid 配合若依的多数据源场景

若依前后端分离版本里有主从数据源配置,甚至有的项目扩展了多租户数据源。这时候你需要对每个数据源都做同样的解密配置。我踩过的坑是:从库里复制了一段配置,但忘记给第二个数据源单独生成密钥对,结果两个数据源用了同一个公钥,运维那边又只保存了一份私钥,后面要换其中一个数据库的密码,就非常被动。我的建议是每个数据源单独生成一套密钥对,私钥命名时加上数据源标识。

5. 常见问题与排查技巧实录

加密配置做完,真正的考验才开始。以下问题我几乎每个都遇到过一次,如果你也在配置过程中翻车,可以直接对照排查。

5.1 密文里的特殊字符把 YAML 搞崩了

Base64 编码的密文可能包含/、+、=这些字符,如果密文没有加引号,YAML 解析时很容易报错。比如ENC(abc/def=ghi)里/可能让 YAML 把它当成别的东西。解决办法就是所有加密字符串统一用双引号包起来:

password: "ENC(8gpm8nbHiQ0m6PKgM7HkF0tMfP4qI1mxKmgUdBvABbY=)"

如果密文是你写进application.properties的,=和#也需要留意,必要时转义。我在实际项目中甚至遇到过密文以=结尾,然后被 Spring 的 properties 解析器截断的情况,那时候用引号包裹也能解决。

5.2 环境变量没传进去,启动报 Decryption failed

Jasypt 的报错信息有时候很不友好,最常见就是Decryption failed或者Unable to decrypt。排查思路按优先级来:先确认环境变量真的传进去了,可以在启动类里临时System.out.println(System.getenv("JASYPT_PASSWORD"))验证;再检查算法是否一致,加密时用的PBEWITHHMACSHA512ANDAES_256跟配置里的jasypt.encryptor.algorithm必须完全一致;最后看 Jasypt 版本,3.0.x 和 3.0.5 之间有一些底层实现调整,如果你在本地生成密文时报错,但线上不报错,或者反过来,多半是版本不一致导致 IV 处理逻辑不同。

5.3 密钥写进了 Git 历史

这是最尴尬也是最常见的问题:配置没问题、加密也没问题,但你的密钥曾经被明文提交到 Git 仓库,哪怕后来改成环境变量读取,历史记录里还挂着。Git 历史里的东西没法真正删除,你能做的只有换密钥。GitHub 官方文档建议用git filter-repo清洗历史,但我可以负责任地告诉你,对绝大多数团队来说,直接换一整套密钥、废弃旧仓库,远比清理历史简单可靠。

5.4 Jasypt 和 Spring Security 的加载顺序冲突

项目里同时引入jasypt-spring-boot-starter和spring-boot-starter-security时,偶尔会遇到 Spring Security 初始化时已经把配置读走导致解密器还没注册的问题。如果遇到这种诡异情况,试试把 Jasypt 的 starter 放在依赖最前面,或者改用spring.factories配置自定义的EnvironmentPostProcessor,让解密动作在 Environment 准备阶段就执行。这个问题在常规单体应用里出现概率不高,但在 Spring Cloud 网关里偶尔会冒出来。

5.5 多环境密钥如何优雅管理

开发、测试、生产环境的密码本来就不同,加密用的密钥如果也各自独立,那配置文件里就要维护三套密文。我的做法是:开发环境的密钥放在.env文件里,测试环境的放在 Jenkins 的凭据管理里,生产环境的放在 Kubernetes Secret 或专门的密钥管理服务里。application-dev.yml、application-test.yml、application-prod.yml里的密文各自对应自己的密钥,互不干扰。这样即使开发环境密钥泄露,也不会影响生产。

6. 再往深走一点:面试和架构层面怎么谈这个话题

配置文件密码加密这件事,其实特别适合当作简历上的一个项目亮点来写。但面试官往往不会只问“你怎么实现的”,他们更关心的是你知不知道边界在哪里。下面分享几个我面试别人时喜欢问的问题,你自己准备的时候也可以按这个思路想。

6.1 谈谈 Jasypt 的ENC()解密机制底层原理

面试官如果想考察你有没有真正理解原理,一般会问:ENC()占位符是在什么时候被替换的?答案是 Spring 的Environment后处理器阶段。Jasypt 通过EnableEncryptablePropertiesBeanFactoryPostProcessor来拦截所有@Value注入、@ConfigurationProperties绑定,当检测到字符串以ENC(开头时,调用加密器解密成明文,再交给 Spring 容器。所以它不是修改了application.yml文件本身,而是在运行时内存里做了替换。理解这层之后,你就能解释为什么配置文件里的密文不能写在 Spring 管不到的静态变量里,也就能理解为什么@Value能解密,而System.getProperty拿到的可能是原始密文。

6.2 为什么对称加密就够用了,生产环境要不要上 KMS

一个很现实的结论是:如果你的威胁模型只是“防止仓库泄露导致密码直接公开”,那 Jasypt + 环境变量就是够用且性价比最高的方案。但如果你所在的公司已经有 KMS 系统,直接把密钥托管到 KMS,应用启动时通过 SDK 拉取,那就更安全一点,因为密钥不再静态存在于服务器文件里,而是每次启动动态获取,还能配合审计日志追踪谁拉取过密钥。缺点是 KMS 故障时应用会启动失败,所以必须做一次缓存兜底。我在生产环境用的是“KMS 优先,本地备份兜底”的策略,兼顾可用性和安全性。

6.3 面试时说清楚“加密不等于绝对安全”

这点是加分项。你要让面试官觉得你是一个考虑过安全边界的人。可以说:任何对称加密方案,只要密钥和应用在同一台服务器上,就存在被拿到权限的运维一键解密的可能。所以更严格的安全体系会把数据库密码分成“存储机密”和“运行机密”两层,存储机密保障仓库泄露不泄露密码,运行机密靠操作系统权限、Tpm、安全模块来保护运行时的密钥。做这块的需求分析时,先回答一个问题:你主要防的是代码仓库泄露,还是防服务器被入侵后的横向渗透?这两个场景的解决方案完全不同。

最后分享一个小技巧:先把明文依赖断干净再上加密

我给团队做配置加密改造的时候,发现最大的阻力不是技术,而是很多老代码会把配置文件里的值通过@Value或者工具类在多个地方引用,加密完之后,有的地方解密成功,有的地方读出来还是ENC(xxx)这种原始格式。排查到最后才发现,有的工具类是静态类,在 Spring 容器初始化之前就被调用了。所以做改造前,建议先全局搜一下配置文件里的敏感字段被哪些类引用,确认所有引用都走 Spring 的配置注入机制,再动手改配置。

第二个建议是分阶段灰度。不要一次性把生产环境的配置全部改成密文,否则出了问题你连回滚的余地都没有。我的做法是在配置中心里加一个新 profile(比如encrypt-prod),让一台实例先切过去,确认运行稳定之后滚动切换剩余实例。出了问题也能立刻切回旧的明文配置,对线上影响降到最低。

关于配置文件密码加密,我踩过的坑比写出来的还多。但每次看到扫描报告里不再出现明文密码,数据源连接串在日志里也是一团密文的时候,那种踏实感是真实存在的。这套东西不复杂,难的是愿意把它当成工程必做项,而不是“等安全部门催了再处理”。希望这篇文章能让你少走几步弯路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询