如果你的项目配置里还写着spring.datasource.password=123456,别急着往下滑,这篇文章就是给你写的。把数据库密码明文放在 application.yml 里,是很多 Spring Boot 项目常见的操作,但同时也是安全审计里排得上号的高风险项。yml 文件一旦进了代码仓库,意味着所有能看到代码的人、所有备份、所有 CI 日志、所有外包拷贝,都可能顺走这个密码。Jasypt 3.0.5 配合 Spring Boot 做配置加密,可以把敏感值变成 ENC(加密串),应用启动时自动解密,代码仓库里看到的永远是一堆密文。这篇实战笔记会用 Spring Boot 2.7.x 作为基础环境,完整走一遍依赖引入、密文生成、配置替换、启动验证、常见报错排查,最后再聊几个生产环境才会用到的进阶细节。
1. 为什么数据库密码不能明文写在 yml 里
1.1 明文配置的泄露途径与真实风险
我见过不少团队,把数据库账号密码直接写在 yml 里,理由无非是图省事、本地好跑、公司内网安全。但真实环境里,泄露路径往往比你想象的更简单:第一,代码仓库的克隆权限通常覆盖开发、测试、运维,甚至外包人员,拿到代码等于拿到数据库凭据;第二,仓库的历史提交里如果出现过明文,修改当前文件并不够,老的 commit 一样能翻出来;第三,编译产物、日志文件、配置备份里也可能残留敏感信息;第四,如果仓库被同步到公共平台或发给外部协作方,密码就彻底敞开了。这些路径互不相关,但目标都是同一个数据库。
更麻烦的是,数据库密码不像接口密钥,被泄露后可能很久都发现不了。等到数据库被脱库、被勒索,才去翻配置历史,往往已经太晚了。安全圈有个说法叫“凭据复用”,大多数人习惯在测试环境、预发环境、生产环境用同一个密码,哪怕你只泄露一个测试库,攻击者也会顺手拿去尝试正式账号。所以这个问题的关键不在于“我们内网很安全”,而在于一旦某个环节失守,代价是整个数据资产。明文密码进仓库,本质上就是把安全防线完全押在别人不犯错上,这显然不现实。
1.2 Jasypt 解决什么问题
Jasypt 不是用来替代权限系统的,它只解决一个核心问题:让敏感配置在静态存储时不以明文出现。它的工作方式可以理解为一个“解密代理”——Spring 启动加载配置文件时,Jasypt 会扫描所有属性值,发现以ENC(开头、以)结尾的字符串,就用指定密码和算法解出明文,再去注入到对应的配置项中。所以你在 yml 里写的 password 是密文,Spring 运行时拿到的才是真实值。
这个机制的好处是:加密逻辑对业务代码完全透明,不需要改任何 @Value 或数据源代码;密文和密钥分离,即使配置文件泄露,只要密钥没有同时泄露,密码就不会被还原。换句话说,Jasypt 并不是让你变得绝对安全,而是把“配置文件泄露 = 数据库密码泄露”这个等式给打破,让攻击者拿到配置后还需要再攻一次密钥管理系统,这中间的难度差距非常明显。
2. 环境与依赖:Spring Boot 2.7.x + Jasypt Starter 3.0.5
2.1 版本选型该怎么定
Jasypt 和 Spring Boot 的集成,实际使用的是jasypt-spring-boot-starter这个第三方 starter,它的版本号是独立的。我这次选 3.0.5,是因为这个版本在 Spring Boot 2.x 上非常稳,文档也最多,遇到问题好查。如果你用的是 Spring Boot 3.x,就要特别谨慎,因为 Spring Boot 3 底层从 javax 迁移到了 jakarta,老版本 starter 的自动配置可能无法直接工作。所以在动手之前,先确认你自己的 Spring Boot 版本。
我的建议是:Spring Boot 2.2 到 2.7 优先使用 3.0.5 starter;Spring Boot 1.x 可以选 2.1.2 之类的旧版本;Spring Boot 3.x 则需要验证兼容性或者考虑自定义方案,不要直接照抄本文的依赖。版本不是越新越好,关键是和自己的项目大版本匹配。这里有一个容易混淆的点:starter 版本号里的 3.0.5,并不是指底层 Jasypt 核心库也一定是 3.x,这个我放到文章后面专门解释。
2.2 引入 Maven 依赖与基础配置
在 pom.xml 中加一个依赖就够了,starter 会把底层 Jasypt 库一起带进来,不需要额外手动引入 jasypt 核心包。
<dependency> <groupId>com.github.ulisesbocchio</groupId> <artifactId>jasypt-spring-boot-starter</artifactId> <version>3.0.5</version> </dependency>接着在 application.yml 里加上 jasypt 加密器的配置。注意这里的jasypt.encryptor.password不要写死成常量,必须用环境变量或外部化配置引用,否则加密等于白做。我通常的做法是在配置里写${JASYPT_ENCRYPTOR_PASSWORD},在部署环境里 export 这个变量。
jasypt: encryptor: password: ${JASYPT_ENCRYPTOR_PASSWORD} algorithm: PBEWITHHMACSHA512ANDAES_256 iv-generator-classname: org.jasypt.iv.RandomIvGenerator算法这里选用PBEWITHHMACSHA512ANDAES_256,这是 Jasypt 新版本里推荐的强算法,带随机 IV,比老旧的 PBEWITHMD5ANDDES 安全得多。iv-generator-classname指定 RandomIvGenerator,可以让每次加密同一个明文得到完全不同的密文,避免通过密文比对推测明文。这两个参数和后面生成密文时的参数必须严格保持一致,大小写也不能错,否则启动时大概率解密失败。
3. 实操:生成密文并替换原有配置
3.1 用 Jasypt 命令行工具生成密文
加密方式有很多种,可以在项目里写单元测试注入 StringEncryptor 来加密,也可以在启动时通过接口生成,但我最推荐的是用 Jasypt 自带的命令行工具,干净利落,不污染项目代码。下载 Jasypt 的 standalone 发布包,解压后能找到对应 jar,执行以下命令:
java -cp jasypt-1.9.3.jar org.jasypt.intf.cli.JasyptPBEStringEncryptionCLI \ input="MyDbPassword123" \ password="MyJasyptSecret" \ algorithm=PBEWITHHMACSHA512ANDAES_256 \ ivGeneratorClassName=org.jasypt.iv.RandomIvGenerator命令中的input是你要加密的数据库密码,password就是 yml 里配置的那个 JASYPT_ENCRYPTOR_PASSWORD,务必保持一致。算法必须和 Spring 端配置完全一致。执行后终端会输出一段以----OUTPUT开头的密文,复制它备用。每次执行结果都不一样,这是正常的,因为使用了随机 IV;只要密钥和算法一致,Jasypt 在解密时都能正确还原。
3.2 把密文写回 application.yml
将原来的明文替换为ENC(密文),格式有严格约束:前缀ENC(和后缀)必须与 starter 的默认规则一致,中间不要加空格,整个值最好用双引号包起来,避免 yml 把括号解析成特殊符号。
spring: datasource: url: jdbc:mysql://localhost:3306/yourdb username: root password: ENC(wlN0f...这里填生成的密文...)Jasypt 不仅支持加密密码,数据库用户名、连接地址、Redis 密码、邮件账号这类敏感信息都可以用同一套方式处理。比如username也可以写成ENC(...),但需要注意解析时机,所有被 Spring 管理的数据源属性、自定义配置、@Value 注入都可以解密,前提是配置项能被 Jasypt 的属性处理器扫描到。如果你的配置是通过自定义类加载的,记得把加载器挂在 Spring 的 Environment 体系里,否则不会自动解密。
3.3 启动验证与解密结果确认
配置完成后,正常启动 Spring Boot。如果一切正常,应用不会打印解密后的密码,只会像平时一样连接数据库。为了确认解密结果,一个简单的做法是在本地启动类里临时写一个 ApplicationRunner,注入 Environment,打印spring.datasource.password。但我的建议是只在本地验证,不要在测试环境乱打日志,避免密文被解密后再次暴露。
启动时如果报错连接超时,不要立刻怀疑 Jasypt,先确认密文是否被正确解密,再看网络和数据库状态。最直接的排查方式是看数据库连接池报错,如果报的是 Access denied for user 'root'@'localhost' (using password: YES),说明解密本身已经完成,只是密码值不对;如果报的是 Cannot load driver class 或 Failed to bind properties,那更像是配置整体加载出了问题,需要回去查 yml 和依赖。
3.4 密钥必须和环境变量绑定
密钥需要单独保存。常见做法是环境变量:Linux 上export JASYPT_ENCRYPTOR_PASSWORD=xxx;Windows 上set JASYPT_ENCRYPTOR_PASSWORD=xxx;容器环境通过 secret 注入;配置中心用配置项注入。切勿把密钥写到 yml 里,哪怕是 test 环境。一个安全原则是:密文可以在代码库,密钥永远不能进代码库。密钥一旦泄露,密文形同虚设,所以密钥的保管是这套方案的核心。
如果团队有专门的密码平台或密钥管理系统,可以考虑把jasypt.encryptor.password指向外部服务获取,后续我们会在进阶部分给出一个自定义加密器的思路。千万不要为了方便,把密钥写在 IDEA 运行配置里还顺手提交到仓库,这相当于把保险柜钥匙贴在保险柜门上,比不加密还危险。
4. 常见坑位与排查实录
4.1 algorithm 不一致导致解密失败
最常遇到的报错是Unable to decrypt data或Decryption failed。产生原因多半是加密时使用的算法、IV 生成器与 Spring 端配置不一致。比如你用 Jasypt 命令行工具加密时没有指定 algorithm,命令行默认使用了旧算法,而应用配置里用的是新算法,结果必然解不开。另一个容易踩的是版本错位:用旧版本库生成的密文,到 3.0.5 starter 的环境里可能因为迭代算法不同而失败。
所以我在生成密文时,会把 algorithm 和 ivGeneratorClassName 作为固定参数写在命令里,而不是依赖默认值。如果已经踩坑了,最简单的解法是:把应用配置里的算法和生成命令里的算法统一改成同一个强算法,重新跑一次加密命令,替换掉旧密文。不要指望同一个密文能在多套算法配置下通用,这是不现实的。
4.2 密文前缀解析失败
如果启动时看到 Jasypt 无法识别ENC()前缀,或者属性值被原样传递,通常是因为属性解析器没有拦住。常见原因有两个:一个是 yml 里密文格式不标准,比如ENC小写、括号前后多空格、引号不匹配;另一个是使用了非 Spring 管理的 PropertySource,比如某些自定义的配置加载器绕过了 Jasypt 的拦截。
建议先把密文整理成标准格式:ENC(这里是一长串密文),确认 yml 没问题后,再排查自己是不是有额外配置覆盖了属性源。另外,如果你同时用了 Apollo 或 Nacos 这类配置中心,要确认配置中心的属性流转回 Spring 的过程没有被截断,否则 Jasypt 在本地文件里生效,但远端配置不生效,这种“部分解密”的现场最让人头疼。
4.3 特殊字符与 YAML 转义问题
数据库密码里如果包含特殊字符,比如&、#、%等,在 yml 中容易触发解析问题。放进ENC()后,由于密文通常是一段 Base64 字符串,本身不含特殊符号,相对安全;但密钥(password)如果包含特殊字符,在命令行或环境变量里需要处理好转义。
命令行里建议把参数用双引号包起来;环境变量里如果含空格或特殊符号,注意 shell 的引号规则。为了避免这种烦恼,我更推荐密钥使用一长串随机字符,只包含字母和数字,不包含任何需要在 shell 中转义的符号。生成随机密钥可以用openssl rand -base64 32,然后手动去掉/+这类符号,或者直接用小写字母加数字的随机串。
4.4 多数据源与多密文处理
如果你的应用配置了主从数据源、多数据源,或者用了 Druid、HikariCP 以外的连接池,加密方式没有任何区别:把每个需要保护的值分别加密,在对应配置位置填入 ENC 密文即可。有一点要注意,Jasypt 默认会对所有属性值做扫描,包括自定义属性,只要值为ENC(...)就会尝试解密。
假如某些业务配置中确实需要存储以ENC(开头的字面量,可以通过参数关闭扫描或者调整前缀规则,但大部分场景用不到。多数据源时,每个密码都要用同一个密钥加密,这样维护成本最低。如果你不小心用不同密钥加密了两个数据源密码,启动时一个能解密、一个报错,那就只能重新生成出错的那个,没有别的捷径。
5. 进阶玩法:自定义加密器与批量加密
5.1 自定义 StringEncryptor Bean
如果不想使用默认的自动配置,或者需要从配置中心、密钥管理服务动态获取密钥,可以自定义一个StringEncryptor。方法是在配置类里声明一个名为jasyptStringEncryptor的 Bean,并在 yml 中设置jasypt.encryptor.bean指向它。
@Configuration public class JasyptConfig { @Bean("jasyptStringEncryptor") public StringEncryptor stringEncryptor() { PooledPBEStringEncryptor encryptor = new PooledPBEStringEncryptor(); SimpleStringPBEConfig config = new SimpleStringPBEConfig(); config.setPassword(System.getenv("JASYPT_ENCRYPTOR_PASSWORD")); config.setAlgorithm("PBEWITHHMACSHA512ANDAES_256"); config.setIvGeneratorClassName("org.jasypt.iv.RandomIvGenerator"); config.setPoolSize(4); encryptor.setConfig(config); return encryptor; } }jasypt: encryptor: bean: jasyptStringEncryptor自定义 Bean 的意义在于:可以把密钥来源从环境变量换成 API 调用,每次启动时动态获取;可以设置密钥池大小;还可以用同一套代码支持多个算法。不过要记得,在 Bean 里不要硬编码密钥,最好还是通过环境变量或外部接口传入。自定义之后,yml 里的默认算法配置可以不再设置,代码里的SimpleStringPBEConfig会作为最终生效的配置。
5.2 批量加密敏感字段的小脚本
如果一个项目里有几十个配置项需要加密,手敲命令行不现实。我一般写一个简短的 shell 脚本,循环读取明文文件,每条调用一次 Jasypt CLI,把输出的密文追加到转换结果文件。脚本不会很长,关键点是:读取密钥使用环境变量,而不是把密钥写在脚本里;加密过程在本地或专门的安全机器上执行;生成的批量密文再复制进 yml。
#!/bin/bash while IFS= read -r line; do if [ -n "$line" ]; then java -cp jasypt-1.9.3.jar org.jasypt.intf.cli.JasyptPBEStringEncryptionCLI \ input="$line" password="$JASYPT_ENCRYPTOR_PASSWORD" \ algorithm=PBEWITHHMACSHA512ANDAES_256 \ ivGeneratorClassName=org.jasypt.iv.RandomIvGenerator | grep "OUTPUT" fi done < plain_credentials.txt脚本执行完记得清理临时明文文件,不要把中间产物提交到仓库。这个脚本只建议在加密机器上执行,不要放到生产环境的定时任务里,否则等于用明文文件换一种方式暴露密钥和密码。批量生成以后,人工抽查两条密文,确保与源明文对应,再整体替换到配置文件中。
5.3 密钥轮换与运维策略
很多团队问能不能直接改密钥让旧密文失效,答案是不能,Jasypt 解密的密钥与加密时绑定,密钥变了旧密文就解不开。轮换的正确姿势是:先在目标环境切换新的密钥,同时重新生成所有敏感配置的密文,然后一次性替换发布。为了避免运维时手忙脚乱,我建议按模块分批替换,并且保留一段时间的旧密钥用于回滚。
这个思路类似证书轮换,不要追求一键完成,要保证可回退。还有一个细节,如果你的配置是从配置中心拉取的,确认配置中心的属性流转路径是否经过 Jasypt,否则解密不会生效。密钥轮换时,最好有一个独立的加密工具包,只跑加密,不接业务流量,这样出问题影响面可控。
6. 版本差异与 Spring Boot 3 兼容性提醒
6.1 starter 版本和底层 Jasypt 库的关系
这里要区分清楚两个版本:jasypt-spring-boot-starter的版本(比如 3.0.5)和底层核心库 Jasypt 的版本。starter 只是把 Spring 的 PropertySource 和 Jasypt 的加解密能力桥接起来,实际负责加密解密的还是 core 库。我们使用命令行加密时,用的是独立发布的 Jasypt 工具包,这个工具包的版本号可能和 starter 不同,不要搞混。
在 Maven 依赖里只需要引入 starter,它会传递引入配套的核心库。如果你发现解密报错,可以用mvn dependency:tree看一下 Jasypt 核心库的具体版本,排除冲突。尤其是项目里如果早年自己引入过 jasypt 核心包,版本和 starter 传递的不一致时,很容易出现莫名其妙的算法兼容问题。遇到这种依赖冲突,统一以 starter 传递的版本为准,把手动引入的旧版本排除掉。
6.2 Spring Boot 3 迁移时需要注意什么
如果你已经在用 Spring Boot 3,切莫直接引入 3.0.5 就完事。由于 Spring Boot 3 的包名从 javax 换成了 jakarta,部分通过反射和代理实现的自动配置可能失效。实测中比较常见的现象是:应用能启动,但配置项没有被解密;或者启动时就报 ClassNotFound。社区有相应的适配方案,和大版本对应的 starter 版本不一定是 3.0.5,需要去项目仓库的 release 信息确认。
如果你的项目不能升级 starter,还有一个兜底思路:通过 Spring 的事件监听器在 Environment 准备完成后手动解密并重新设置属性,或者把加密逻辑放到配置中心的解密接口里。这不是一条愉快的路,但能救急。总体来说,新项目直接上 Spring Boot 3 的话,建议先做一个小 Demo 验证 Jasypt 的可用性,再决定要不要全量铺开,不要在核心业务链路上临时试错。
最后分享一个我自己的使用习惯:我会在项目的 README 中写清楚JASYPT_ENCRYPTOR_PASSWORD这个环境变量如何配置,但绝不写上具体值;在本地开发时,我用 .env 文件配合启动脚本,并把 .env 加入 gitignore;在交付代码前,会用 grep 扫一遍 yml 里有没有残留的明文 password。这套东西并不是什么高深技术,但能在第一次被扫描工具盯上之前,帮你少挨一次骂。如果你手头项目里还有明文密钥,建议趁今天赶紧替换掉,别等出了问题再后悔。