你有没有经历过这样的场景:数据库账号密码明晃晃地写在jdbc.properties里,代码仓库一拉下来,连刚入职的实习生都能一眼看到生产库地址和口令?我在维护某个后台管理系统的时候,就是这么踩坑的。开发环境密码弱一点还好,一旦生产库密码跟着配置一起进了代码仓库,再被安全扫描工具爆出来,那感觉不是一般的酸爽。后来我决定把jdbc.properties里的敏感字段全部加密,前端配置文件看到的是密文,程序运行起来照样正常连接数据库,问题才算真正解决。这篇分享就围绕 Spring MVC 项目下的jdbc.properties配置文件加密展开,讲清楚方案选型、加密原理、实操步骤和排查经验,适合正在给传统 Spring 工程做安全加固的开发者参考。
1. 为什么要加密 jdbc.properties——三个真实场景告诉你这个问题有多严重
1.1 明文数据库密码是埋在配置文件里的定时炸弹
Spring MVC 项目里,jdbc.properties一般长这样:
jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/mydb?useUnicode=true&characterEncoding=utf8 jdbc.username=root jdbc.password=123456这个文件太常见了,常见到很多人都不当回事。但只要你稍微回想一下这几个场景,就会知道平铺明文的代价有多大:
- 代码仓库权限管理不当,离职同事的账号没及时吊销,拿着历史版本就能看到数据库账号密码;
- 项目备份包、日志上报、聊天工具里粘贴的配置片段,明文密码会顺着这些渠道扩散;
- 生产库、测试库、开发库如果共用一套账号密码——这种我见过不止一次——明文泄露的影响范围会被直接放大。
我是怎么意识到这件事的?某次给老项目做安全扫描,扫描报告把jdbc.properties里的明文密码直接标注成高危漏洞。当时运维拿着报告找到我,我才发现配置文件的权限链比想象中脆弱得多。从那以后,我的原则很简单:凡是数据库密码这类敏感信息,能不进明文配置就坚决不进。
1.2 别再用 Base64“伪装”加密了
群里经常有人分享“加密经验”,点开一看就是Base64.encode(password)。这里必须说清楚:Base64 只是编码,不是加密。任何人拿到那串字符,复制到在线解码工具里一秒就能还原,和明文没有本质区别。
我还见过更离谱的做法,写死一个异或算法,把密码处理成不可读字符串。但这种方案的问题在于,算法本身和密钥都躺在代码里,只要有人能反编译或者翻到那个工具类,整个“加密”就形同虚设。
真正意义上的配置文件加密,需要做到两点:
- 密文里看不出原始内容的任何痕迹;
- 解密必须依赖一个“不写死在代码里”的外部密钥。
满足这两点的方案,才算有资格进入生产环境。
1.3 加密方案的选型逻辑——为什么首选 Jasypt
市面上能做配置文件加密的工具不少,我按 Spring MVC 这个特定场景筛选,标准只有三个:
- 是否支持非 Spring Boot 的传统 XML 配置环境;
- 依赖是否轻量,不能为了加密给老项目引入一堆无关组件;
- 是否支持把主密码放在外部,而不是硬编码在代码里。
按这个标准筛下来,Jasypt(Java Simplified Encryption)是最合适的。它对 Spring 3.1+ 的老工程有专门的集成包,通过PropertyPlaceholderConfigurer或PropertySource机制拦截配置加载过程,把ENC(密文)自动解密成明文,对业务代码完全透明。相比自己手写解密器,Jasypt 最大的好处是算法成熟、社区资料多、出问题好排查。后面我也会讲自定义方案的思路,但如果你只是想快速把问题解决掉,Jasypt 基本是首选。
2. Jasypt 加密的原理与核心解密机制
2.1 Jasypt 怎么做到“密文可配置、明文不落地”
Jasypt 的核心思路其实很朴素:在 Spring 解析配置文件、把数据源信息交给连接池之前,插入一个解密环节。你写在jdbc.properties里的password字段不再是明文,而是一段密文,比如:
jdbc.password=ENC(7zV2Yd1HrGt4qA9xP6mNlC3sK8wR5tE)Spring 容器启动时,Jasypt 的PlaceholderConfigurer会识别出ENC(...)这种格式,用配置好的主密码和算法解出真实密码,再注入到数据源 Bean 里。业务层拿到的DataSource始终是配置好密码的实例,开发人员不会在代码里看到解密过程,也不会在日志里看到明文密码。
这个“拦截 + 替换”的设计有一个非常明显的好处:解密的触发点被限定在 Spring 容器装配阶段。之后即使有人 dump 出运行时内存,或者从日志里找到配置片段,也拿不到原始密码,因为密文只在装配的那一瞬间解开一次。
2.2 主密码与算法参数的协同关系
Jasypt 的加密强度,一靠算法,二靠主密码。主密码不是数据库密码本身,而是一个用来派生加密密钥的“口令”。没有这个口令,就算拿到完整密文,也不知道怎么解出原文;有了口令,再加上同样的算法和随机盐,才能复现原文。
我习惯把主密码比喻成保险柜钥匙:数据库密码是保险柜里的文件,主密码决定保险柜能不能打开。这个类比在团队沟通里特别好使,运维一听就明白为什么不能把主密码写进代码。
算法方面,老版本 Jasypt 1.9.x 默认走PBEWithMD5AndDES,强度偏弱。我建议有条件的话换成PBEWithHMACSHA512ANDBASE64(Jasypt 2.x 支持),或者使用 AES-256 的 PBE 方案。配好算法后,加密配置里必须写清楚:
jasypt.encryptor.algorithm=PBEWithHMACSHA512ANDBASE64 jasypt.encryptor.password=${JASYPT_MASTER_PASSWORD}这里有一个特别容易踩的坑:生成密文时用的算法和运行时配置的算法必须一致,否则解密必然失败。我踩过一次,生成密文用的默认算法,部署机上配置了更强的算法,结果启动直接报解密错误,排查了半天才发现是算法不匹配。
2.3 Spring MVC 与 Spring Boot 引入方式的差异
很多人搜“jasypt 配置文件加密”找到的资料都是给 Spring Boot 写的,一上来就是jasypt-spring-boot-starter,结果放在 Spring MVC 老工程里怎么都跑不起来。核心原因是两者的加载机制不一样:
- Spring Boot 通过自动配置和
EnvironmentPostProcessor出手; - Spring MVC 要基于
PropertyPlaceholderConfigurer或PropertySourcesPlaceholderConfigurer手动接入。
在 Spring MVC 工程里,正确做法是引用jasypt-spring31(对应 Spring 3.1+)或jasypt-spring4(对应 Spring 4.x),然后在spring-context.xml里配置一个EncryptablePropertyPlaceholderConfigurer,把jdbc.properties交给它加载。这一步如果搞错,会出现两种典型问题:
- 配置没被解密,数据源拿到的还是
ENC(...)字符串; - 容器启动时无法识别自定义配置器,直接报 Spring 配置解析失败。
我建议动手前先确认工程的 Spring 具体版本,再决定引哪个包。这一步做对,能省掉后面大量的排查时间。
3. 手把手实操:在 Spring MVC 项目中实现 jdbc.properties 加密
3.1 环境准备与依赖引入
先交代一下我当时的实验环境:JDK 1.8、Spring 4.3.18、Maven 3.6,传统 SSM 结构。在这种组合下,需要引入的 Jasypt 依赖有两个:核心包jasypt-1.9.3,以及 Spring 集成包jasypt-spring4。注意,jasypt-spring4并不是给 Spring Boot 用的,它对应的是 Spring 4.x 的 XML 配置型工程。
pom.xml里类似这样(版本号根据自己环境调整):
<dependency> <groupId>org.jasypt</groupId> <artifactId>jasypt</artifactId> <version>1.9.3</version> </dependency> <dependency> <groupId>org.jasypt</groupId> <artifactId>jasypt-spring4</artifactId> <version>1.9.3</version> </dependency>如果你的 Spring 正好是 5.x,对应的是jasypt-spring5;Spring 3.1 到 3.x 用jasypt-spring31。选错版本最常见的报错是NoClassDefFoundError,因为它的兼容层编译时依赖的 Spring 版本和你工程里实际版本不一致。
依赖到位后,我强烈建议先别急着写加密配置。可以先在本地写一个最简的 Spring 工程做验证,确认“解密 + 数据源连接”全链路通顺,再把这个改动搬到真实项目里。老工程往往有各种历史包袱,一步到位很容易被别的问题干扰判断。
3.2 生成密文,替换 jdbc.properties 中的明文
生成密文有两个常见途径:命令行和 Java 代码。命令行方式最直接,适合一次性操作。把 Jasypt 的 jar 包下载到本地后,用以下命令生成:
java -cp jasypt-1.9.3.jar org.jasypt.intf.cli.JasyptPBEStringEncryptionCLI input="Mysql@2024" password="MasterKey#9527" algorithm=PBEWithMD5AndDES注意,命令里的password参数就是主密码。跑完会输出一堆环境信息,其中加密结果在 OUTPUT 段:
----OUTPUT---------------------- 7zV2Yd1HrGt4qA9xP6mNlC3sK8wR5tE把这段输出包装成ENC(7zV2Yd1HrGt4qA9xP6mNlC3sK8wR5tE),写回jdbc.properties:
jdbc.password=ENC(7zV2Yd1HrGt4qA9xP6mNlC3sK8wR5tE)这里有个容易踩的坑:如果明文密码里含有空格、$、#这类特殊字符,命令行参数最好整体加双引号,否则 Shell 会把部分内容截断或者做变量替换。强烈建议生成后人工核对一次密文长度,不同算法的密文长度有固定规律,如果明显偏短,多半是input参数被 Shell 吞掉了一部分。
3.3 加载解密配置并启动验证
Spring MVC 这边,关键是让容器用支持解密的配置器加载jdbc.properties。以 XML 配置为例,在spring-context.xml里加入:
<bean id="propertyConfigurer" class="org.jasypt.spring4.properties.EncryptablePropertyPlaceholderConfigurer"> <constructor-arg> <bean class="org.jasypt.encryption.pbe.StandardPBEStringEncryptor"> <property name="algorithm" value="PBEWithMD5AndDES"/> <property name="password" value="MasterKey#9527"/> </bean> </constructor-arg> <property name="locations"> <list> <value>classpath:jdbc.properties</value> </list> </property> <property name="fileEncoding" value="UTF-8"/> </bean>注意,password属性值只是演示,生产环境不建议直接写在这里。更正规的做法是用环境变量占位:
<property name="password" value="${JASYPT_MASTER_PASSWORD}"/>然后在启动脚本里通过-DJASYPT_MASTER_PASSWORD=MasterKey#9527传入,或者设置系统环境变量。这样主密码就不再进入代码仓库,真正做到了“主密码外部化”。
配好之后,数据源 Bean 里仍然用${jdbc.password}引用占位符,Jasypt 会在解析占位符时自动解密并返回原文。启动工程后,如果数据库连接成功,说明解密链路已经打通。
验证时我建议看两点:一是启动日志里不要出现任何包含明文密码的输出;二是用调试模式在DataSource初始化前断点查看 password 属性的值,确认它已经是真实密码而不是ENC(...)。
3.4 多环境切换与主密码注入的最佳实践
老项目多半有 dev、test、prod 三套环境,jdbc.properties也会跟着环境走。加密之后,每套环境的密文其实可以用不同的主密码分别生成,再把主密码放到对应环境的基础配置里。我常用的做法是:
- 在 Jenkins 或部署脚本里为每个环境定义一个环境变量,例如
JASYPT_MASTER_PASSWORD; - Spring 的
propertyConfigurer从环境变量里读取主密码; - 环境的
jdbc.properties里的密文,由该环境专属的主密码生成。
这样做的价值在于:开发环境即使泄露了主密码,也只是开发库的密码;生产主密码单独保管,由运维或安全负责人持有,开发人员即使看到生产配置的密文,也无法反向解出明文。
另一个好处是,数据库密码轮换变得很轻松——只需要重新生成密文,不需要改动 Java 代码,重启应用即可生效。这块后面我还会单独讲。
4. 进阶方案:自定义 PropertySource 解密器,摆脱对框架的依赖
4.1 Spring Environment 加载配置的时机
Jasypt 用起来虽然省事,但有些团队出于依赖控制、安全合规的要求,不愿意引入第三方加密组件。这种情况下,自己写一个轻量解密器完全可行。前提是理解 Spring MVC 里配置加载的两个关键抽象:PropertySource和PropertyResolver。
Spring 3.1 开始,Environment里维护了一个PropertySource列表,properties 文件(包括jdbc.properties)被加载后,会以PropertySource的形式存在,占位符解析时由PropertyResolver逐个查找。如果我们能在一个合适的时机,把PropertySource里的密文字符串替换成明文,就能实现同样的“拦截 + 解密”效果。
下手时机通常有两个:一是自定义PropertySource,在getProperty时实时解密;二是继承PropertySourcesPlaceholderConfigurer,在加载后、解析前统一处理。前者更优雅,但对 Spring 内部机制要求更高;后者实现简单,适合快速落地,也是我推荐大多数团队采用的方案。
4.2 自定义 EncryptablePropertyPlaceholderConfigurer 的实现
如果你用的是 Spring 3.1 以上的版本,可以通过继承PropertySourcesPlaceholderConfigurer来做。核心思路是重写一个受保护的方法,在配置被真正用于替换占位符之前,把ENC(...)格式的字符串解成明文。
简化后的代码类似这样:
public class EncryptablePropertyPlaceholderConfigurer extends PropertySourcesPlaceholderConfigurer { private static final String ENC_PREFIX = "ENC("; private static final String ENC_SUFFIX = ")"; private final String masterPassword; public EncryptablePropertyPlaceholderConfigurer(String masterPassword) { this.masterPassword = masterPassword; } @Override protected void convertProperties(Properties props) throws IOException { Enumeration<?> propertyNames = props.propertyNames(); while (propertyNames.hasMoreElements()) { String propertyName = (String) propertyNames.nextElement(); String propertyValue = props.getProperty(propertyName); if (propertyValue != null && propertyValue.startsWith(ENC_PREFIX) && propertyValue.endsWith(ENC_SUFFIX)) { String ciphertext = propertyValue.substring( ENC_PREFIX.length(), propertyValue.length() - ENC_SUFFIX.length()); props.setProperty(propertyName, decrypt(ciphertext, masterPassword)); } } super.convertProperties(props); } }然后在 XML 配置里用这个自定义 Bean 替换原来的propertyConfigurer,其他代码全部不动。这个方案看起来简单,但它的覆盖能力其实很强:只要是通过这个配置器加载的配置文件,不管是jdbc.properties还是redis.properties,只要值符合ENC(...)格式,都会被自动解密。
4.3 用 AES 算法手写一个轻量解密工具类
自定义解密器里那个decrypt方法,用什么算法取决于你的需求。如果公司内部没有统一加解密平台,建议直接用 JDK 自带的 AES,避免引入额外依赖。下面是一个常见写法:
public class AesEncryptor { private static final String ALGORITHM = "AES"; private static final int KEY_LENGTH = 16; private static final String TRANSFORMATION = "AES/ECB/PKCS5Padding"; public static String decrypt(String content, String masterPassword) throws Exception { byte[] keyBytes = Arrays.copyOf( masterPassword.getBytes(StandardCharsets.UTF_8), KEY_LENGTH); SecretKeySpec keySpec = new SecretKeySpec(keyBytes, ALGORITHM); Cipher cipher = Cipher.getInstance(TRANSFORMATION); cipher.init(Cipher.DECRYPT_MODE, keySpec); byte[] decoded = Base64.getDecoder().decode(content); byte[] decrypted = cipher.doFinal(decoded); return new String(decrypted, StandardCharsets.UTF_8); } }这里需要说明几点:AES/ECB/PKCS5Padding 属于对称加密,虽然 ECB 模式在严格密码学标准里不推荐,但作为配置文件密文的轻量级保护,多数团队可以接受。如果对安全性要求更高,可以换成 CBC 模式,把初始化向量 IV 一并存入密文。
另一个容易被忽略的点是masterPassword必须能被截取或补齐成 16 字节、24 字节或 32 字节,否则会抛InvalidKeyException。我建议在主密码右侧补齐到 16 字节,密钥管理上保持一致。
自己做解密器的最大好处是完全可控:不引依赖、不担心版本冲突、逻辑透明。最大风险也一样明显:算法实现、密钥管理、兼容性全都要自己负责。我的建议是,如果团队里有熟悉密码学的同事,自定义方案值得尝试;如果只是想快速解决安全问题,Jasypt 仍然是更省心的选择。
5. 常见问题与排查技巧实录
5.1 启动时就报错:解密失败还是占位符未解析
我在落地这个项目时,遇到过的启动报错大概分两类。
第一类是解密失败,典型报错是org.jasypt.exceptions.EncryptionOperationNotPossibleException,原因基本就是主密码不对、算法不一致、密文被截断。排查思路很简单:先用同一套主密码和算法,在命令行重新解密这段密文,能解出来就说明配置没问题;命令行都解不出来,那就是密文或主密码的问题,别急着怀疑 Spring 配置。
第二类是占位符未解析,日志里会看到数据源 Bean 的 password 属性还是字面量的${jdbc.password}。出现这种情况,多半是 Spring 容器里存在多个PropertyPlaceholderConfigurer,后加载的配置器覆盖了 Jasypt 那个。Spring 规范要求一个 Spring 上下文中只能定义一个PropertyPlaceholderConfigurer,这是最容易踩的坑。
我当时的项目里,公共模块和业务模块各自配置了一个占位符处理器,Jasypt 的配置被后者覆盖,结果就是死活不解密。解决方法是全局只保留一个,或者用setOrder控制加载顺序。
5.2 特殊字符与中文乱码的坑
properties 文件在 JDK 里默认按 ISO-8859-1 读取,如果你的数据库密码、URL 里包含中文、日文等非 ASCII 字符,很可能会出现乱码。这个坑在加密场景里尤其隐蔽:明文状态下乱码一眼就能看出来,加密后密文本身是 ASCII 字符,乱码问题就转移到了加密前的明文上,你会发现解密后的密码和数据库实际密码对不上。
解决办法是给PropertyPlaceholderConfigurer明确设置fileEncoding:
<property name="fileEncoding" value="UTF-8"/>但如果多个配置文件混在一起,有的用 UTF-8 有的用默认编码,全局指定会引入新问题。我的建议是:数据库连接串和密码尽量只用 ASCII 字符,实在避不开中文再考虑编码设置。另外,生成密文前先用一个包含特殊字符的完整明文测试一轮,交给数据库连接池验证,通过后再替换正式配置。
5.3 多数据源场景下的加密隔离
如果工程里同时配置了主库、从库、缓存库等多个数据源,jdbc.properties里会有多组账号密码。Jasypt 的ENC(...)格式对多数据源天然支持,每个密码字段各自加密即可。但我建议在结构上做隔离,不要把所有密码堆在同一个文件里。
可以按数据源拆成多个 properties 文件,分别用不同的主密码加密,再在 Spring 配置里分别配置对应的EncryptablePropertyPlaceholderConfigurer。这样即使某一套主密码泄露,也不会波及其他数据源。
还有一种情况是项目里用了 Druid 连接池。Druid 自己提供了ConfigTools静态加密工具和 connection-properties 解密支持,如果你已经深度使用了 Druid,可以考虑直接走 Druid 的解密链路,而不必再引入 Jasypt。两种方案不要混用,否则可能会出现“外层密文解完、内层 Druid 又解一次”导致密码错乱的问题。
5.4 密钥丢失、轮换与团队协作问题
主密码一旦丢失,等于所有密文作废,最坏的情况是应用无法启动。所以从第一天开始,就要把主密码纳入团队密钥管理体系。我常用的做法是:开发环境主密码可以放在团队文档里共享,测试和生产环境主密码放到运维的凭据管理工具里,应用启动时由部署脚本读取并注入。这样既不影响日常联调,又保护了生产主密码。
数据库账号密码轮换时,如果你只改了数据库侧的密码,没有同步更新jdbc.properties里的密文,应用会在一段时间后出现连接认证失败。正确流程是:先在运维侧更新数据库密码,然后用新的明文配合同一把主密码重新生成密文,更新配置文件并重启。整个过程不需要修改 Java 代码,也不需要重新发版,这是配置外置加解密带来的额外好处。
最后分享一个我自己的小习惯:每次改完加密配置,我会在那个环境里先执行一次最小启动验证——只启动数据源 Bean,不加载全部业务 Bean,确认解密成功、连接池初始化正常后再继续后续流程。这个习惯帮我过滤掉了很多“加密本身没问题,但容器环境有问题”的干扰项。
配置加密这件事,真正难的不是把明文变成密文,而是让整个解密链路在复杂的老工程里始终稳定可靠。你按这套思路走下来,至少能少踩我当年踩过的大部分坑。