Nacos是怎么样和Jasypt结合的?
一、从一个尴尬的场景说起
假设你是一个微服务项目的开发,某天安全部门发来一份整改通知:
检查发现,Nacos 配置中心中
spring.datasource.password为明文存储,存在敏感信息泄露风险,请于本周内完成整改。
你打开 Nacos 控制台一看:
1 | spring: |
改就改呗,把密码挪到环境变量里?但运维说不行,环境变量在 K8s 里也是明文;放到 K8s Secret?那还得改部署流程。折腾一圈,同事丢过来一个方案:用 Jasypt 加密,Nacos 里只存密文。
于是配置变成了这样:
1 | spring: |
应用启动后,程序拿到的是明文 MySecret@123,但 Nacos
里看到的只是一串乱码。完美。
这背后的功臣,就是 Jasypt。而它和 Nacos 的配合,并不只是”加密存储”这么简单——里面还藏着一个关于”加载顺序”的经典问题。这篇文章就从原理到实践,把整个链路讲清楚。
二、Nacos 配置管理是怎么实现的?
要理解 Jasypt 怎么和 Nacos 结合,得先知道 Nacos 的配置是怎么”进”到你的 Spring 应用里的。
1. 启动时的引导阶段
Spring Cloud 应用启动时,会先创建一个引导上下文(Bootstrap Context)。它的使命很单纯:在应用主上下文创建之前,把远程配置中心里的配置拉下来。
加载顺序大致是这样:
1 | Spring Boot 启动 |
关键点:Nacos
的连接信息(server-addr、password
等)必须在引导阶段就能拿到,否则根本连不上服务器。这就是为什么它们通常写在
bootstrap.yml 里——这个文件的加载时机足够早。
如果需要细究的话,就是 Nacos 实现了 NacosPropertySourceLocator
而 PropertySourceLocator 会被 PropertySourceBootstrapConfiguration 在 prepareContext 阶段调用(PropertySourceBootstrapConfiguration 继承了 ApplicationContextInitializer)
这个阶段可以看一下 Spring Boot 启动时的文章,大致是 createEnvironment -> createContext -> prepareContext -> refreshContext
2. 启动后的动态刷新
配置拉下来之后不是就完事了。Nacos 客户端会和服务端保持一个长轮询(Long Polling) 通道(新版使用Grpc长连接,对后续理解无影响):
- 客户端不断问服务端:“我关心的这些配置变了吗?”
- 没变就挂起等待;变了就立即把新配置拉回来
- 更新本地
Environment和@RefreshScope的 Bean(对于@RefreshScope注解的理解和使用,可以单开一篇文章了,现在只需要知道@RefreshScope标注的 bean,会在收到配置更新时间后刷新)
这就是你在 Nacos 控制台改完配置、刷新一下页面就能看到效果的原因。
3. 小结
Nacos 配置管理的本质是:引导阶段拉配置 + 运行时长轮询刷新。它介入 Spring 的时机非常早,早到很多”常规”的初始化逻辑都还没跑。这为后面的问题埋下了伏笔。
三、Jasypt 是什么?
Jasypt 全称 Java Simplified Encryption,是一个 Java
加解密库。而在 Spring Boot
场景下,大家通常用的是它的封装:jasypt-spring-boot-starter。
它的用法极其简单——你只管写密文,它负责解密:
- 用工具把明文加密,得到
ENC(xxx)格式的字符串 - 把密文写进配置文件
- 启动时提供一个解密密钥(
jasypt.encryptor.password) - 之后程序里任何
@Value("${xxx}")或environment.getProperty("xxx")拿到的,都是解密后的明文
一个最小的例子
1 | # application.yml |
1 |
|
它是怎么”透明解密”的?
Jasypt 的核心手段不是”修改配置文件”,而是包装 PropertySource。
Spring 的 Environment 里维护着一堆
PropertySource(比如 application.yml
对应一个、bootstrap.yml
对应一个、系统环境变量对应一个……)。每个 getProperty()
调用最终都会落到某个 PropertySource 上。
Jasypt 通过一个 BeanFactoryPostProcessor(简称
BFPP),把这些 PropertySource 全部包装成
EncryptablePropertySourceWrapper。之后任何
getProperty() 调用都会经过这层包装:
1 | 调用 getProperty("spring.datasource.password") |
这就是它”透明”的原因——不管是 @Value
注入、Environment.getProperty(),还是 Nacos
客户端内部读配置,只要走了 Environment
这条路,就都会被自动解密。
四、两者结合:加解密与存储分发各司其职
把 Nacos 和 Jasypt 放在一起,分工非常清晰:
| 角色 | 职责 |
|---|---|
| Jasypt | 负责加解密:在应用侧把 ENC()
还原成明文 |
| Nacos | 负责存储和分发:保存密文,通过长轮询推送给客户端 |
结合方式有两种场景:
场景一:加密 Nacos 里存放的业务配置
这是最常见的用法。数据库密码、Redis 连接串、第三方 API Key 等敏感配置,全部加密后存进 Nacos:
1 | # Nacos 上的 cpc-console-server-local.yaml |
应用启动后,Jasypt 自动解密,业务代码无感知。这一步没有任何时序问题——因为 Nacos 只是把密文当普通字符串传过来,解密是应用侧的事。
场景二:加密 Nacos 自身的连接密码
这就麻烦了。你的 bootstrap.yml 里写着:
1 | spring: |
问题来了:
- Nacos 客户端要在引导阶段连服务器,此时需要密码的明文
- Jasypt 的解密器要在主上下文初始化时才生效,此时还没轮到它
如果 Nacos 拿到的是字面量
ENC(...),它会拿着这串”乱码”去连服务器,结果自然是认证失败。
这就是”先有鸡还是先有蛋”的经典问题:要解密需要 Jasypt 跑起来,要 Jasypt 跑起来需要主上下文初始化,要主上下文初始化需要先从 Nacos 拉到配置,要连 Nacos 需要先解密……
看似死循环。但实际情况是,在某些版本组合下,它是能跑通的。为什么?
五、Jasypt 是怎么”抢跑”的?
答案藏在 jasypt-spring-boot-starter
的一个隐藏入口里,以及 Spring Cloud
的启动机制里。我们一点点把链路接起来。
1. Spring Boot 启动时,先扫 spring.factories
SpringApplication 在构造阶段(还没执行
run() 之前),就会通过 SpringFactoriesLoader
扫描 classpath 下所有 jar 包的
META-INF/spring.factories 文件,找出所有注册了
ApplicationListener 接口的实现类,并实例化它们。
1 | // SpringApplication 构造函数中 |
只要你的 classpath 上有 spring-cloud-context(引入 Nacos
配置中心时会间接带入),它的 spring.factories
里就注册了:
1 | # spring-cloud-context/META-INF/spring.factories |
所以 BootstrapApplicationListener 在
SpringApplication 构造完成的那一刻,就已经躺在
listeners 集合里了。
2. 两个监听器的 Order 值决定了谁先跑
ApplicationEnvironmentPreparedEvent
发布后,所有监听该事件的监听器会按 Order
值排序执行,值越小越优先:
| 监听器 | Order 值 | 执行顺序 |
|---|---|---|
BootstrapApplicationListener |
Ordered.HIGHEST_PRECEDENCE + 5 |
先执行 |
ConfigFileApplicationListener |
Ordered.HIGHEST_PRECEDENCE + 10 |
后执行 |
这个顺序是有意为之的。BootstrapApplicationListener
必须抢在 ConfigFileApplicationListener 之前,先把 Bootstrap
上下文建起来,去加载 bootstrap.yml 并连接 Nacos。如果让
ConfigFileApplicationListener 先跑,本地的
application.yml
会先被加载,配置的优先级覆盖关系就乱了。
3. BootstrapApplicationListener 做了什么
BootstrapApplicationListener
收到事件后,会创建一个全新的
SpringApplication(我们称之为 Bootstrap 应用),并调用它的
run() 方法,从而创建一个独立的 Bootstrap
ApplicationContext:
1 | // BootstrapApplicationListener 核心逻辑 |
4. 嵌套的启动流程——关键的一环
这里是最容易绕晕的地方。BootstrapApplicationListener
不是”执行完就结束了”,它启动的 Bootstrap 应用会重新走一遍完整的
SpringApplication.run() 流程,包括:
- 再次发布
ApplicationEnvironmentPreparedEvent事件 - 再次触发所有监听器(
BootstrapApplicationListener会检测到已在 Bootstrap 过程中,直接跳过,防止无限递归) - 再次调用
ConfigFileApplicationListener,从而加载bootstrap.yml,并执行所有EnvironmentPostProcessor
1 | 主应用 SpringApplication.run() |
5. Jasypt 的 BootstrapConfiguration 在 Bootstrap 应用中被加载
Bootstrap 应用启动时,会去读取 spring.factories 中
org.springframework.cloud.bootstrap.BootstrapConfiguration
这个 key 下配置的类。
而 jasypt-spring-boot-starter 的
spring.factories 里正好注册了:
1 | # jasypt-spring-boot-starter/META-INF/spring.factories |
这个类的源码很简洁:
1 |
|
注意
matchIfMissing = true——不写这个配置也默认生效。
它 @Import 的配置类会向 Bootstrap 上下文注册一个
static 的 BFPP,在 Bootstrap 应用 refresh()
时执行,把 Bootstrap Environment 的所有 PropertySource
包装成解密壳。
至此,Bootstrap Environment 具备了自动解密能力。 之后 Nacos 读密码时,读到的是明文。
六、另一种注入 jasypt 密码的方式:自定义 EnvironmentPostProcessor
上面讲的是把 jasypt.encryptor.password 写在
bootstrap.yml
里的方案。但实际生产中,解密密钥本身也不该明文写在配置文件里——否则加密就失去意义了。
于是就有人想:能不能通过环境变量传入,在代码里动态注入?
一个真实的实现
1 |
|
注册方式:
1 | # META-INF/spring.factories |
为什么这个方案能成功?
看到这里,你可能会有一个疑问:
ConfigFileApplicationListener(Order = HIGHEST + 10)比BootstrapApplicationListener(Order = HIGHEST + 5)后执行,所以DefensePostProcessor是在BootstrapApplicationListener之后才跑的。那 Jasypt 在 Bootstrap 上下文里执行 BFPP 时,jasypt.encryptor.password还没被加进去,怎么解密?
这个问题问得非常好,答案是前面讲的”嵌套启动流程”:
DefensePostProcessor不是在主应用的启动流程里执行的,而是在 Bootstrap 应用的启动流程里执行的。Bootstrap 应用是一个完整的SpringApplication,它同样会走prepareEnvironment→ 发布事件 →ConfigFileApplicationListener响应 → 执行所有EnvironmentPostProcessor这条链路。顺序完全正确:在 Bootstrap 应用内部,
- 先执行
DefensePostProcessor,从环境变量YNLZX读到 Base64 编码的密钥,解码后注入jasypt.encryptor.password - 然后 Bootstrap 应用
refresh(),Jasypt 的 BFPP 执行,包装 PropertySource - 之后 Nacos 读
bootstrap.yml里的ENC(...)密码时,解密条件已全部具备
- 先执行
BootstrapApplicationListener防止了递归:它在 Bootstrap 应用的启动过程中再次收到ApplicationEnvironmentPreparedEvent时,会检测到当前已处于 Bootstrap 过程中,直接跳过,不会无限创建新的 Bootstrap 应用。
两种方案的对比
| 方案 | jasypt.encryptor.password 的来源 |
生效时机 | 密钥是否落地 |
|---|---|---|---|
写在 bootstrap.yml 里 |
配置文件直接读取 | Bootstrap 应用加载 bootstrap.yml 时 |
❌ 密钥明文写在配置里 |
自定义 EnvironmentPostProcessor |
从环境变量读取,Base64 解码后动态注入 | Bootstrap 应用的 EnvironmentPostProcessor 阶段 |
✅ 密钥只在运行时存在 |
两种方式最终效果一样——都是让
jasypt.encryptor.password 在 Bootstrap 应用
refresh() 之前就存在于 Environment 中,从而让 Jasypt 的
BFPP
能正常工作。但后者的安全性明显更高:密钥不进代码库、不进配置文件,只通过环境变量在运行时注入。
⚠️ 注意:
DefensePostProcessor必须通过META-INF/spring.factories注册,否则不会被ConfigFileApplicationListener加载执行。
七、这个机制不是永久的——版本陷阱
上面这套机制能成立,有一个前提:你的项目还在使用 Spring Cloud 的 bootstrap 机制。
旧版本(Spring Cloud Alibaba ≤ 2023.x)
bootstrap.yml默认启用spring.factories里的BootstrapConfiguration会被加载- Jasypt 能在引导上下文抢跑
- Nacos 连接配置写在
bootstrap.yml
新版本(Spring Cloud Alibaba 2025.x+)
- 官方废弃了 bootstrap 支持,强制走 Spring Boot 2.4+
的
ConfigData机制 spring.factories里的BootstrapConfiguration入口不再被触发- Jasypt 的”抢跑”失效
- Nacos 拿到的永远是字面量
ENC(...),连接直接 403 - Nacos 连接配置迁移到
application.yml,用spring.config.import导入
1 | # 新版本写法 |
那新版本里 Nacos 密码怎么加密?
既然 Jasypt 的解密赶不上了,最务实的方案是:用环境变量或 JVM 参数直接传明文。
1 | spring: |
启动时:
1 | java -jar app.jar -DNACOS_PASSWORD=MySecret@123 |
Nacos 连接密码用环境变量,Nacos 内部的业务配置继续用 Jasypt 加密——这是新版下最稳妥的组合。
八、一个容易踩的坑:${VAR:ENC(...)}
的假象
最后说一个实战中容易迷惑人的写法:
1 | password: ${NACOS_PASSWORD:ENC(Q9PNYb+QmZOd9cdlX8b3zXK7S3eGKsQlY5z2sCewoRo=)} |
有人可能会发现:明明没配环境变量,怎么也能连上?
要小心,这里有两种可能:
可能一:环境变量确实存在。
${VAR:默认值} 的语义是——优先读环境变量
NACOS_PASSWORD,只有它不存在时才用冒号后面的默认值。如果你的部署环境里已经注入了这个变量(K8s
Secret、启动脚本、CI/CD 变量等),Nacos
拿到的就是明文,ENC() 那串密文根本没被用到。
可能二:环境变量不存在,但老版本 + Jasypt 抢跑。
在旧版本下,Jasypt 会把 ENC(...)
解密,返回明文。此时即使环境变量没配,也能连上。
如何辨别?
去掉环境变量、同时升级到新版试试——如果连接立刻失败,说明之前一直靠的是”环境变量覆盖”这个障眼法,ENC()
只是个摆设。
这个写法本身没有错,但它会掩盖问题。如果你以为”ENC() 生效了”,实际靠的却是环境变量,一旦环境变量被移除或版本升级,故障会突然爆发。
九、总结
把整篇文章的核心串起来:
- Nacos 配置管理:引导阶段拉配置,运行时长轮询刷新。
- Jasypt:通过包装
PropertySource实现透明解密,任何走Environment.getProperty()的读取都会被自动解密。 - 两者结合:Nacos 存密文、Jasypt 在应用侧解密,业务代码无感知。
- Nacos 密码加密的难题:Jasypt 通常在主上下文才生效,赶不上 Nacos 的连接时机。
- 旧版本的解法一:Jasypt 通过
spring.factories注册BootstrapConfiguration,在引导上下文里也跑一次 BFPP,从而”抢跑”成功。 - 旧版本的解法二:自定义
EnvironmentPostProcessor,从环境变量动态注入jasypt.encryptor.password,密钥不落地,安全性更高。它能生效的关键在于 Bootstrap 应用会走一遍完整的启动流程,EnvironmentPostProcessor在这个嵌套流程里被执行,早于 Jasypt 的 BFPP。 - 监听器顺序:
BootstrapApplicationListener(Order = HIGHEST + 5)先于ConfigFileApplicationListener(Order = HIGHEST + 10)执行,这是保证 Bootstrap 上下文先建立的关键。 - 新版本的现实:bootstrap 机制被废弃,抢跑失效,Nacos 连接密码请改用环境变量或 JVM 参数传入。
- 一个隐蔽的坑:
${VAR:ENC(...)}可能让你误以为加密生效了,实际靠的是环境变量覆盖。
一句话记住它:
Nacos 负责”送”,Jasypt 负责”解”。旧版本里 Jasypt 通过
BootstrapApplicationListener触发的嵌套启动流程提前蹲守在引导上下文里,等着 Nacos 来读;密钥要么写在bootstrap.yml,要么通过自定义EnvironmentPostProcessor从环境变量动态注入。新版本里蹲守点没了,Nacos 的密码就老老实实用环境变量吧。
希望这篇能帮你把这条链路彻底理清。如果项目还在旧版本、跑得好好的,那就先别急着升级——搞清楚自己依赖的是哪套机制,比盲目升级重要得多。