数据库密码写在配置里,这在不少团队里是个“公开的秘密”。多年前我参与过一个交付项目,代码已经在客户现场部署了,结果客户做安全巡检,直接把仓库里的 application.yml 翻出来,指着明文数据库密码问要怎么处理。当时场面非常尴尬。后来我们把密码全部换掉,引入 Jasypt 做配置项加密,才把这个问题彻底解决。
Jasypt(Java Simplified Encryption)是一个开源的 Java 加密库,它最常见的用法就是配合 Spring Boot,把数据库密码、Redis 密码、第三方接口密钥这些敏感信息,以ENC(xxxxxx)这种密文形式放在配置文件里,运行时再解密。对业务方来说,它解决的是“配置文件泄露后,敏感信息不直接裸奔”的问题;对开发同学来说,它几乎不改变原有代码结构,引入依赖、改配置、启动即可。
这篇文章适合正在做 Java/Spring Boot 项目的后端开发、技术负责人和安全运维看。我会从为什么需要加密说起,讲清 Jasypt 的加密原理,然后给出一套可以直接落地的操作流程,最后把我在实操中踩过的坑和排查思路整理出来。如果你想在自己的项目里把数据库密码“藏起来”,跟着这一篇走就够了。
1. 为什么会有“加密数据库密码”这个需求
1.1 配置明文的风险到底有多大
不是矫情。明文密码一旦进了代码仓库,风险就是持久且扩散的。第一,仓库会被克隆给同事、外包、协作方,甚至被某些自动同步工具传到不该去的地方,密码跟着传播。第二,日志、错误堆栈、接口文档里常不经意带上连接串,URL 里自带用户名密码。第三,员工离职、第三方人员接触权限收回后,可读仓库历史的人依然能看到密码。一个数据库密码一旦泄露,最直接的影响就是数据被拖库、被篡改、被勒索。你可能觉得“内网环境怕什么”,但内网也有横向穿透的可能,你要把泄露面控制到最低,而不是赌没有人看到。这也是不少行业合规审计里对敏感信息加密的典型要求——不是“建议”,是“必须”。
我见过最离谱的一个项目,配置里不光数据库密码是明文,连 Redis 密码、对象存储的 secretKey、短信平台账号密码全都在同一个 properties 文件里,随手一翻全部暴露。那不只是配置文件的问题,是团队对敏感信息根本没有基础的敬畏。后来我们做改造,第一件事就是把所有配置项过一遍筛子,凡是带密码、密钥、token 的字段全部标记成敏感字段。
1.2 为什么选了 Jasypt 而不是自研加密或 KMS
先想清楚需求边界:我们不是要做一个加密系统,只是不想让密码以明文形式躺在配置和代码仓库里。这个目标下有三条路:
- 自研工具类:把 AES 加解密写成一个静态方法,配置文件里放 AES 密文。但问题很快会出现:密钥放哪?如果密钥也被硬编码在类里或者配置文件里,那加密等于脱裤子放屁;如果放环境变量,又要自己做一套密钥分发、版本兼容、多环境切换逻辑,工作量不小,而且每个项目都复制一遍。
- 引入云 KMS 或者自建密钥管理系统:专业、安全、功能强大,但需要维护额外服务、网络访问、权限体系。对很多中小团队来说,重了。如果项目还没到那个规模,用它属于杀鸡用牛刀。
- Jasypt 集成:它把 PBE(基于口令的加密)流程封装好,与 Spring Boot 无缝对接,密文可以直接放到配置里,密钥从外部注入。轻、够用、生态成熟。
我当时的选择就是第三种。你如果问 Jasypt 能做到 KMS 那样细粒度的权限控制和审计吗?做不到。但大部分业务系统不需要做到那一步。先解决“密码不裸奔”这个刚需,后面真到合规审计要求 KMS 级别,再演进也不迟。另外,Jasypt 最大的优势是同项目里的成员几乎零学习成本:只要会写配置、会配环境变量,就能把整个链路跑起来。
2. Jasypt 的加密原理,搞懂这几件事
2.1 它不是魔法,就是标准的 PBE
Jasypt 的核心是一套 PBE(Password Based Encryption,基于口令的加密)流程,说白了就是你给它一个口令(密码),它通过一系列变换生成加密密钥,再用这个密钥去加密目标字符串。整个链路可以拆成这么几步:
- 从随机字节生成了一个盐值(salt);
- 把盐值和用户口令一起送入密钥派生函数(KDF),得到真正的对称加密密钥;
- 利用该密钥和初始化向量(IV)对明文做对称加密(例如 DES、AES);
- 把盐值、IV、密文组合起来,再经过 Base64 编码,输出成看上去像乱码的密文。
为什么要随机盐?因为如果只做“口令→固定密钥→固定密文”,同一个密码每次加密结果都一样,攻击者可以预先算好彩虹表,看到密文反查明文。加入随机盐后,同样一个数据库密码,每次加密出来的密文都不相同,彩虹表基本失效。
为什么要迭代次数?KDF 里会循环做多次哈希运算,比如 PBKDF2 默认迭代很多次,目的就是让暴力破解变得非常贵——攻击者猜一个口令要跑 N 次哈希,而不是一次。Jasypt 的默认迭代次数比较保守,你可以按自己安全要求调,但太高的迭代也会增加启动时解密耗时,要平衡。
2.2 算法和参数怎么选
Jasypt 支持的算法来自 JDK 的 JCE(Java Cryptography Extension)。老版本 Jasypt 默认用的是PBEWithMD5AndDES,这个组合在今天是偏弱的,DES 密钥长度只有 56 位,MD5 也有已知弱点,如果攻击者拿到密文并且口令偏弱,存在被破解的风险。新版本(1.9.x 以上)默认推荐PBEWithHMACSHA512AndAES_256。这个组合用 AES-256 做实际加密,密钥通过 HMAC-SHA512 派生,安全强度足够日常业务使用。
需要在 JDK 8+ 下才能直接支持 AES-256,JDK 7 及以下还需要替换 JCE 无限强度策略文件,否则会报illegal key size这个经典错误。如果你的项目还在 JDK 7 上跑,优先升级 JDK,而不是降级用 DES,否则加密强度会大打折扣。
参数上还有几个可以关注的:
| 配置项 | 推荐值 | 说明 |
|---|---|---|
| algorithm | PBEWithHMACSHA512AndAES_256 | AES-256 实际加密,HMAC-SHA512 派生密钥,JDK 8+ 原生支持 |
| iv-generator-classname | org.jasypt.iv.RandomIvGenerator | 每次加密生成随机 IV,避免相同明文产生相同密文 |
| keyObtentionIterations | 可配置,如 10000 | 迭代次数越高,暴力破解成本越高,但解密耗时也上升 |
| poolSize | 默认 1 | 使用 PooledPBEStringEncryptor 时可调大,适合多线程解密 |
常规项目直接用推荐值即可,不用过度调优。一个容易被忽略的配置是iv-generator-classname,如果你用 AES_256 算法,要设置成org.jasypt.iv.RandomIvGenerator,否则启动时会提示找不到合适的 IV 生成器。
3. 实操:把数据库密码改成 Jasypt 加密
3.1 环境准备与依赖引入
假设你的项目是基于 Spring Boot 2.7,JDK 8,Maven 构建。第一步引入依赖:
<dependency> <groupId>com.github.ulisesbocchio</groupId> <artifactId>jasypt-spring-boot-starter</artifactId> <version>2.1.2</version> </dependency>如果是 Spring Boot 3.x,要使用 3.0.x 版本的 starter(比如 3.0.5),因为底层对配置绑定、自动装配机制有变化,版本用错很容易出现自动配置不生效。反过来,Spring Boot 2.x 项目直接上 3.0.x 也可能有问题,所以引入前先查一下对应关系。这个 starter 的作用是:自动探测配置文件里的ENC(...)内容,在 Spring Environment 初始化阶段解密这些占位符,然后把解密后的值交给 DataSource、Redis 等组件使用。整个过程对业务代码完全透明,不用在每个注入点改代码。
有一点要厘清:starter 本身不会把你的数据库密码自动加密,它只负责在运行时解密。密文需要你提前用工具生成。到 3.2 节,我再演示三种生成方式。
3.2 生成密文的方式
方式一:写一个临时测试类生成。
package com.example.common; import org.jasypt.encryption.pbe.StandardPBEStringEncryptor; public class GenerateCipher { public static void main(String[] args) { StandardPBEStringEncryptor encryptor = new StandardPBEStringEncryptor(); encryptor.setAlgorithm("PBEWithHMACSHA512AndAES_256"); encryptor.setPassword(System.getenv("JASYPT_ENCRYPTOR_PASSWORD")); encryptor.setIvGeneratorClassName("org.jasypt.iv.RandomIvGenerator"); String ciphertext = encryptor.encrypt("你的数据库明文密码"); System.out.println("加密结果:ENC(" + ciphertext + ")"); // 校验:能解密回明文就说明这轮没问题 String decrypted = encryptor.decrypt(ciphertext); System.out.println("解密结果:" + decrypted); } }这里我特意用System.getenv("JASYPT_ENCRYPTOR_PASSWORD")而不是直接在代码里写口令,原因是生成密文的机器和启动应用的机器如果口令不一致,后面会解密失败;而如果口令写在代码里,那这份代码本身就成了泄露源。生产里真正落地时,要在部署机器或 CI 环境配置好同一个环境变量,保证一致。
方式二:用官方命令行工具。Jasypt 官方提供的 cli 包自带加密入口,不需要写代码:
java -cp jasypt-1.9.3.jar org.jasypt.intf.cli.JasyptPBEStringEncryptionCLI \ input="你的数据库密码" \ password="你的加密口令" \ algorithm=PBEWithHMACSHA512AndAES_256 \ ivGeneratorClassName=org.jasypt.iv.RandomIvGenerator控制台会输出密文。命令行方式适合一次性生成、或者写脚本批量生成和密钥轮换。不过要注意命令行参数password=xxx本身也会被 shell 历史记录、进程列表里的参数暴露,生产环境慎用,更稳的是从 stdin 里读取,用完即走。
方式三:借助单元测试。把生成逻辑放进测试类,配合项目内统一的加密器 Bean,保证生成密文使用的算法、盐、IV 策略和运行环境完全一致。我个人比较推荐这种方式,因为它天然避免“生成时配置 A、运行时配置 B”的不一致问题。
3.3 配置 Spring Boot 与启动参数
生成密文后,把原来配置文件的明文密码替换成带 ENC 前缀的密文:
spring: datasource: url: jdbc:mysql://localhost:3306/your_db?useUnicode=true&characterEncoding=utf8 username: root password: ENC(IQ8Z8j9FJQlDq9Xp7I6WD5kG0x8r58ysym5K6Ld9yBY=)然后在配置里定义 Jasypt 自己的参数。常见写法是:
jasypt: encryptor: password: ${JASYPT_ENCRYPTOR_PASSWORD} algorithm: PBEWithHMACSHA512AndAES_256 iv-generator-classname: org.jasypt.iv.RandomIvGenerator这句password: ${JASYPT_ENCRYPTOR_PASSWORD}的意思是从环境变量里取密钥,而不是把密钥写在这里。Jasypt 在 Spring Boot 中的解密时机是在占位符解析阶段,它会读取jasypt.encryptor.*属性,用其中的口令去初始化加密器,然后处理ENC(...)。如果你不是把 jasypt.encryptor.password 放在配置文件里,而是启动时用-Djasypt.encryptor.password=xxx传入,效果是一样的,Spring 的 Environment 优先级会覆盖配置文件中的同名项。
关键原则:不管怎么配置,真正意义上的“密钥本体”绝不能写入代码仓库。它应该来自环境变量、启动参数、部署平台 Secret 机制或公司的密码管理系统。
到这里,整个接入过程核心链路已经打通。接下来讲团队接入时最容易踩的坑,这些我基本都踩过一遍。
4. 接入过程中最常见的坑,一个一个排
4.1 启动报错“Unable to decrypt”或“EncryptorInitializationException”
这两个错是接入 Jasypt 后最常遇到的,原因也高度雷同:加密器初始化时拿不到口令,或者拿到的口令与你加密时用的不一致。排查时先按下面的矩阵过一遍:
| 现象 | 可能原因 | 处理方式 |
|---|---|---|
| 启动报 EncryptorInitializationException | 环境变量未配置,或配置 key 写错 | 检查 JASYPT_ENCRYPTOR_PASSWORD 是否存在、拼写是否一致 |
| 启动报 Unable to decrypt: IllegalStateException | 加密时的口令和解密时不一致 | 用同一口令重新生成密文,或修正启动参数 |
| 启动报 invalid algorithm | 算法名与 JCE 不匹配,如拼写错误 | 使用标准写法,如 PBEWithHMACSHA512AndAES_256 |
| 启动报 No such IVGenerator | AES 算法配套 IV 生成器没配置 | 配置 iv-generator-classname 为 RandomIvGenerator |
然后是定位技巧。先把 Jasypt 的日志级别调到 DEBUG:
logging: level: org.jasypt: DEBUG com.ulisesbocchio: DEBUG启动后看日志里打印的jasypt.encryptor.password是否为空。如果为空,多半是环境变量没传进去;如果能看到一串掩码后的值,再对比是不是同一个口令。记住一点:密文本身查不出“是用什么口令加密的”,唯一能验证的方法就是拿同样的口令去走一遍解密,这跟钥匙配锁是一个道理。
4.2 密文里的特殊字符把 YAML 解析搞崩
BASE64 编码后的密文可能包含+、/、=等符号。在 YAML 里,明文值如果直接写,=出现在值中多数时候没问题,但+开头或结尾的特殊情况可能被解释成 tag 或类型转换。最稳妥的做法是给所有加密值加单引号:
password: 'ENC(abc+def==/ghi)'单引号能够避免 YAML 把内容当成数字、布尔值或其它类型。如果你用的是.properties文件,:和=也要注意,最好补上转义,或者干脆统一换成 YAML 格式。这个坑很小,但一旦触发,报错信息看着像配置问题,实际却不在 Jasypt 而在于配置解析层,很容易绕弯路。
另外,Spring Boot 对ENC(...)的识别是通过 PropertySource 上的占位符解析完成的,所以不要在密码里嵌套ENC(ENC(...))这种形式,Jasypt 不支持递归解密,遇到这种情况直接报错。
4.3 密钥轮换不是改一个字段那么简单
数据库密码泄露后要换密钥,常见操作是把 Jasypt 口令改了、把配置里的密文也重新生成了,然后选择低峰期重启应用。这里有个忌讳:只改口令不重新生成密文。因为旧密文是用旧口令加密的,新口令根本解不开。我自己刚上手时干过这事,重启后一脸懵。
核对的检查清单通常是:
- 确认要轮换的口令和需要更新的密文清单(数据库、Redis、第三方密钥等);
- 用新口令重新生成所有密文,逐一替换到配置文件;
- 在测试环境用一模一样的环境变量完成一次完整启动验证;
- 部署窗口内切换环境变量、重启应用,观察连接池日志和业务调用;
- 如果担心回滚,提前把旧口令、旧密文备份在密码管理器中,回滚时一并恢复。
如果团队规模大、密文数量多,还可以考虑用自定义 StringEncryptor,在加密器里准备一个主密钥加一串历史密钥,解密时逐个尝试。这个方案能实现新旧密钥共存,适合平滑过渡,但会稍微增加代码复杂度,别为了酷而过度设计。
4.4 日志泄露、生成密文的历史记录
一个很容易被忽略的泄露渠道是日志。我们之前排查过一个启动失败问题,为了调试在代码里打印加密后的密文和明文密码,结果日志打到文件里,运维同学翻日志直接看到了数据库账号密码。后来我们定了条规矩:代码里不允许打印任何密码明文,调试时用掩码工具,日志框架再配一层敏感词过滤。
加密生成用的命令行 history 也是泄露点,前面说过password=xxx参数会留在 shell 历史里,所以口令尽量从环境变量或 stdin 传入,不要在 history 里留痕。还有一个细节:如果加密用的口令太弱,比如123456、password、项目名加年份,那暴力破解的成本会很低,Jasypt 也救不了你。密钥必须要足够随机、足够长,最好直接由密码管理器生成。
5. 进阶:密钥管理与落地规范
5.1 密钥不落盘,那放哪里
最简方案是环境变量。部署平台(比如常见的 CI/CD 系统、容器平台的 Secret 机制)可以在创建应用时注入环境变量,应用启动时读取,仓库里只保留配置模板:
spring: datasource: password: ENC(配置模板中用密文占位) jasypt: encryptor: password: ${JASYPT_ENCRYPTOR_PASSWORD}这里的模板可以提交仓库,但JASYPT_ENCRYPTOR_PASSWORD的具体值只存在于部署环境的变量表里。如果项目有用到配置中心,也可以把密文放进配置中心,但密钥仍然走环境变量注入。注意不要又绕回去把密钥明文放进配置中心,那等于把钥匙和锁放在同一个抽屉。
多环境(开发/测试/生产)最好分别使用不同的口令和不同的密文。开发环境可以宽松些,产线口令由负责人统一管理,定期轮换。这样即使测试环境被脱库,生产口令也不受影响。
5.2 团队落地规范建议
实际落地时,光有技术方案还不够,团队共识更重要。我梳理一下我们当时的落地动作,可以作为参考:
- 建立敏感字段清单。梳理所有配置项,哪些包含口令、token、密钥,统一标注为敏感字段,禁止明文出现在配置文件的任何形态里。
- 代码评审加一个“敏感信息”检查点。评审时重点看新增配置项是否用了 ENC,是否引入了新的明文密码。
- 写一个扫描脚本放进 CI。用正则扫描仓库里形如
password:、secret:、accessKey:等字段,非 ENC 开头的就报警。这个脚本不需要很复杂,能起拦截就行。 - 密文和密钥分开管理。密文可以提交仓库,密钥只放密码管理器和部署平台,团队成员需要密钥时走申请流程,不通过聊天工具传来传去。
- 新项目从初始化时就启用 Jasypt,老项目按模块分批改造。一次把所有项目全量改完,风险太大,分模块走反而稳。
还有个细节:加密范围不一定只限定数据库密码。Redis 密码、消息队列账号密码、对象存储密钥、第三方开放平台密钥,凡是敏感配置都可以统一走同一套加密器。这样团队只需维护一条规则,不用每次判断“这个字段要不要加密”。
5.3 自定义加密器的扩展思路
Jasypt 默认的 StandardPBEStringEncryptor 对大多数项目已经够用。如果你们有特殊安全要求,比如密钥需要从密钥管理系统动态获取、解密时要走审计日志,可以实现一个StringEncryptor接口,手动创建 Bean:
package com.example.common; import org.jasypt.encryption.pbe.PooledPBEStringEncryptor; import org.jasypt.encryption.pbe.config.SimpleStringPBEConfig; import org.jasypt.iv.RandomIvGenerator; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class JasyptConfig { @Bean(name = "jasyptStringEncryptor") public PooledPBEStringEncryptor jasyptStringEncryptor() { PooledPBEStringEncryptor encryptor = new PooledPBEStringEncryptor(); SimpleStringPBEConfig config = new SimpleStringPBEConfig(); config.setPassword(System.getenv("JASYPT_ENCRYPTOR_PASSWORD")); config.setAlgorithm("PBEWithHMACSHA512AndAES_256"); config.setIvGenerator(new RandomIvGenerator()); config.setPoolSize(4); encryptor.setConfig(config); return encryptor; } }注册了这个 Bean 之后,Spring Boot 里的ENC(...)就会自动走这个自定义加解密器。你可以把config.setPassword(...)替换成从密钥管理系统拉取密钥的逻辑,这样密钥完全不出安全域。要注意的是,这个 Bean 的名字必须是默认约定的jasyptStringEncryptor,如果你改名了,要么在 Jasypt 配置里显式指定,要么启动时会创建默认加密器,导致自定义逻辑不生效。
再补充一下性能说明:数据源密码、Redis 密码每个组件通常在启动阶段只解密一次,连接池运行期不会反复解密字符串,所以性能影响几乎可以忽略。如果你在某个高频循环里频繁调用解密方法,才会暴露出性能问题,这时建议把解密后的值缓存起来,而不是每次现算。
6. 实操体会与落地建议
6.1 一次让我彻底改变习惯的教训
我做了很多年项目,对加密数据库密码这件事,最大的体会是:它解决的不仅是技术问题,更是团队对敏感信息的态度问题。
我刚开始做的时候,其实也抱着“配置写在仓库里,内网小团队根本没人看”的心态。直到有一次代码仓库被误设为公开,所有配置瞬间暴露,数据库密码、密钥、内部接口信息全部裸奔。那次之后我才彻底改变习惯。把 Jasypt 引进来,并不是为了炫技,它只是把“别把密码写在明处”这条规矩通过工程化手段固化下来。有了它,开发同学的配置文件里不再出现明文密码,新同学入职看到的默认模板就是密文形式,规范自然变成习惯。
6.2 建议今天就做的三件事
如果你们项目还没启用任何配置加密,我的建议是从今天就开始,哪怕只加密数据库密码也好。操作路径很清楚:引入 starter、生成密文、替换配置、环境变量注入密钥,跑通一条链路后把规范复制到所有项目。中间遇到问题不要慌,排错的核心思路就两条:一是密钥口令必须一致,二是算法配置必须一致。这两点对了,大部分所谓“Jasypt 解密失败”基本都能解决。
最后再分享一个小技巧:保存密文时,可以把明文和密文的对应关系放在密码管理器的备注里,标注好生成时间和所使用的口令版本。这个动作在三个月后密钥轮换时会帮你省下大量时间。如果你之前已经踩过配置文件明文密码的坑,应该知道这种“事后记一笔”有多重要。