作者: 柳遵飞
前言
Spring Cloud 框架在微服务领域被广大开发者所使用,@Value 是每位开发者都会接触到的注解,在 SpringBean 中可以通过 Value 注解引用 application.properties 属性,实现配置代码分离,提升应用代码部署的灵活性,但无法在运行期动态更新配置。Nacos 是一款集服务发现和配置管理功能的中间件产品,其中配置中心可以实现运行期配置实时生效,将工程本地的属性文件配置在 Nacos 中,在应用中做一些配置上的改动就可以轻易集成 Nacos 实现配置的动态刷新,工程依赖的属性多种多样,其中把有一些敏感数据配置在中心化的 Nacos 中可能会存在一些安全性层面的顾虑,Nacos 也有方法来应对这个问题,本次我们就对以上问题进行介绍。
本文将以如下步骤展开:
- 集成 Nacos 实现动态配置更新
- 集成 KMS 零代码改造实现敏感配置加密
- Spring Cloud+Nacos 工作原理介绍
SpringCloud 应用配置常规用法
在一个 Spring Cloud 应用中,可以在 Bean 中通过 @Value 注解来引用 Spring 上下文中的属性值,可以引用环境变量,JVM 参数以及我们常见的 application.properties 配置文件中的属性。
以下是该种用法简易实例:
application.properties:
app.switch=true app.threadhold=0.8
一个简单的 Spring Bean:
@Component public class AppConfig{ @Value("${app.switch:false}") boolean switch; @Value("${app.threadhold}") double threadhold; }
AppConfig 可以被其他的 SpringBean 引用,可以正常获取到配置在 application.properties 中的 app.switch 和 app.threadhold 属性。
当我们需要修改 app.switch 和 app.threadhold 的值时,我们需要修改配置文件中的内容并对应用进行重启,当我们需要频繁修改某些业务参数时,重启应用的效率较低。
集成 Nacos 实现配置动态刷新
以下我们将介绍如何在 Spring Cloud 应用中结合 Nacos 实现配置动态更新。
Spring 在 2.4.x 版本开始,引入了 spring.config.import 参数,可以自定义外部的属性源,通过 Spring Cloud Alibaba 组件可以实现将 Nacos 中的配置添加为 Spring 的属性源之一,因此在一个 Spring Bean 中也可以通过 Value 注解读取到 Nacos 中的配置。
以下我们将 Spring Cloud Alibaba 简称为 SCA。
1. pom 中添加 SCA nacos config 依赖
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> <version>${spring.cloud.alibaba.version}</version> </dependency>
组件的版本名称和 spring boot 版本相关,可以根据 sca 官网的版本说明选择对应的版本:https://sca.aliyun.com/docs/2021/overview/version-explain/
2. 初始化 Nacos 配置
可以选择开源 nacos 或者购买商业化 MSE Nacos 版本,以下的图示为商业化 Nacos。
假设我们的应用为支付业务的应用,应用名为 pay。
在 Nacos 实例中可以创建 dataId=pay-application.properties,group=core 的配置。
3. 修改应用工程中的 application.properties
spring.config.import=nacos:pay-application.properties?group=core&refreshEnabled=true spring.cloud.nacos.config.server-addr={server addr}
添加 sping.config.import 参数将 Nacos 中 dataId=pay-application.properties,group=core 的配置添加为属性源,refreshEnabled=true 表示当 Nacos 中的配置变更时,需要同步刷新 Spring 中的属性源。
添加 spring.cloud.nacos.config.server-addr 参数指定连接的 Nacos 地址。
删除工程本地 application.properties 的 app.switch,app.threadhold 参数。
4. Spring Bean 中添加 RefreshScope 注解
@Component @RefreshScope public class AppConfig{ @Value("${app.switch:false}") boolean switch; @Value("${app.threadhold}") double threadhold; }
在业务代码中仍然使用 Value 注解来引用 Spring 上下文中的配置,但需要在 Bean 上添加 RefreshScope 注解,只有添加该注解,Spring 才会在内部属性源更新时将属性刷新到当前的 Bean 中。
重启应用后,此时我们在 Nacos 中对配置 pay-application.properties 中属性进行修改,应用程序中 AppConfig 的参数值就会动态更新。
集成 KMS 实现配置无感加密
上一节中我们通过集成 Nacos 实现了 SpringCloud 应用的配置动态更新,应用中的配置类型多种多样,其中某些配置具有较高的敏感性,比如数据库的连接地址,用户名密码,一些第三方组件的秘钥,Token 以及其他业务功能中敏感配置等等,这些配置的安全性非常重要,如果泄漏可能会对业务造成不可估量的影响。这些数据在 Nacos 中是存放在 pay-application.properties 中,以下是示例:
以下示例中的敏感参数均为模拟数据:
dataId=pay-application.properties,group=core:
# 数据库配置 spring.datasource.driver-class-name=com.mysql.jdbc.Driver spring.datasource.url=jdbc:mysql://localhost:3306/myapp spring.datasource.username=user001 spring.datasource.password=pass!@#$% # 秘钥Token等 secret and token app.secret=GFYIdryujixxx key.token=eedsjpp56hko8h # 业务参数 app.switch=false app.threadhold=0.8
SpringBean:
@Component public class SecretConfig{ @Value("${app.secret}") String secret; @Value("${app.token}") String token; @PostConstruct public void init(){ //init client use token and secret } }
@Component public class AppConfig{ @Value("${app.switch:false}") boolean switch; @Value("${app.threadhold}") double threadhold; }
对于其中数据库密码,Token 等数据,通常会有更多安全性层面的考虑,比如这些敏感配置存储在 Nacos 中安全性是否可以保证,应用访问 Nacos 传输过程中数据是否存在泄漏可能性,敏感配置和普通的业务配置能否设置不同的读写权限,要实现以上安全性的要求,业务的代码是否可以尽量低成本改造等等。
要实现以上的安全性诉求,我们要做到以下几点:
1. 敏感配置在 Nacos 需要加密存储,不能直接明文存储
2. 敏感配置在传输过程中需要加密传输,防止中间网络设备通过抓包方式窃取数据。
3. 应用中的业务代码不能感知配置是否加密,仍需要按照之前的方式读取属性值,降低改造成本。
以下我们将介绍如何通过集成 KMS 实现零代码改造实现上述诉求。
加密配置拆分
如果我们期望将普通业务配置和敏感配置分离,我们可以 dataId=pay-application.properties,group=core 的配置进行拆分,将敏感数据单独拆分出一个独立的加密 Nacos 配置,比如 dataId=cipher-kms-aes-256-pay-application.properties,group=secret,用于存放数据源 token 等相关的敏感配置。为了防止解密配置和普通配置属性文件中的属性值重复,我们可以在加密配置中的属性值统一加上 encrypted 前缀。
Nacos 中的配置
1. dataId=cipher-kms-aes-256-pay-application.properties,group=secret
# 数据库配置 encrypted.spring.datasource.driver-class-name=com.mysql.jdbc.Driver encrypted.spring.datasource.url=jdbc:mysql://localhost:3306/mydatabase encrypted.spring.datasource.username=user001 encrypted.spring.datasource.password=pass!@#$% # 秘钥Token等 secret and token encrypted.app.secret=test_GFYIdryujixxx encrypted.key.token=test_eedsjpp56hko8h
2. dataId=pay-application.properties,group=core
原先的 pay-application.properties 中的属性暂时保持不动,等应用程序侧的所有节点引入新配置 cipher-kms-aes-256-pay-application.properties 之后,再对其做变更。
工程内配置改造
1. 引入 MSE 插件扩展包
加密的配置在存储和网络传输过程中都是密文,因此需要在应用侧支持解密的功能,在 pom 中引入解密插件。
<dependency> <groupId>com.alibaba.nacos</groupId> <artifactId>nacos-client-mse-extension</artifactId> <version>1.0.4</version> </dependency>
2. 调整项目工程下的 application.properties
在 spring.config.import 添加新加配置 cipher-kms-aes-256-pay-application.properties,以及设置 KMS 初始化相关参数。
spring.config.import=nacos:cipher-kms-aes-256-pay-application.properties?group=secret&refreshEnabled=true,nacos:pay-application.properties?group=core&refreshEnabled=true spring.cloud.nacos.config.server-addr={server addr} # 设置客户端NacosClient访问KMS所需参数 spring.cloud.nacos.config.kms_region_id=cn-hangzhou spring.cloud.nacos.config.kmsEndpoint=kst-xxx.cryptoservice.kms.aliyuncs.com spring.cloud.nacos.config.kmsVersion=v3.0 spring.cloud.nacos.config.kmsClientKeyFilePath=clientKey_hangzhou.json spring.cloud.nacos.config.kmsPasswordKey=10xxxd1d spring_cloud_nacos_config_kmsPasswordKey=10xxxd1d spring.cloud.nacos.config.kmsCaFilePath=clientKey_hangzhou.json
客户端 NacosClient 访问 KMS 所需参数和 KMS 版本相关,具体步骤及后续更新见 MSE 官方文档:https://help.aliyun.com/zh/mse/user-guide/create-and-use-encrypted-configurations
修改配置重启业务应用完成后,此时应用程序读取的还是 Nacos 中 pay-application.properties 的属性值,但是此时 encrypted. 前缀的相关属性已经存在于 Spring 的上下文中。
加密配置迁移
当前应用程序重启完成之后,我们对 Nacos 的配置做如下修改:
dataId=pay-applicaition.properties,core=core
# 数据库配置 spring.datasource.driver-class-name=${encrypted.spring.datasource.driver-class-name} spring.datasource.url={encrypted.spring.datasource.url} spring.datasource.username=${encrypted.spring.datasource.username} spring.datasource.password=${encrypted.spring.datasource.password} # 秘钥Token等 secret and token app.secret=${encrypted.app.secret} key.token=${encrypted.key.token} # 业务参数 app.switch=false app.threadhold=0.8
将原先在 pay-applicaition.properties 中的敏感属性以 ${} 方式引用 cipher-kms-aes-256-application.properties 中的属性 key。其中 cipher-kms-aes-256-pay-application.properties 中的属性并不会被应用程序代码中直接引用,而是在 pay-application.properties 通过配置嵌套的模式间接引用,程序代码中本质上还是读取的 pay-application.properties 中的属性。
过程中,我们只对工程中的配置文件做了改造,而业务代码层面没有做任何改动。改造完成后,cipher-kms-aes-256-pay-application.properties 配置中的内容在 Nacos 服务端,传输过程中以及应用本地的缓存中都是密文形式,在业务应用进程中,NacosClient 内部会和 KMS 交互完成密文解密成明文。
配置动态刷新工作原理介绍
通过以上改造,我们就完成了 Spring Cloud+Nacos 实现配置动态刷新的功能,下面我们将介绍 Spring Cloud + Nacos 实现动态配置刷新的工作原理。
启动加载机制
Spring Bean 的初始化需要读取 Nacos 中的配置,因此 Nacos 初始化的过程是在所有 Spring Bean 初始化之前进行。Spring Clound Aliababa 组件会根据当前的 application.properties 参数对 Nacos 进行初始化,从 Nacos Server 加载配置,并构建为 NacosPropertySource。在此阶段中,Spring 也可以从 JVM 或者环境变量中读取参数,因此 Nacos 初始化所需的参数也可以通过 JVM 参数和环境变量进行设置,比如 Nacos server 的地址,命名空间 namespace,鉴权相关的 accessKey 及 secretKey 等。
在构建好完整属性源之后,Spring 会进入 Bean 的初始化流程中,只有在该阶段正常完成了 Nacos 的初始化以及 Nacos 配置的加载,Bean 才可以正常读取到 Nacos 中的配置。
动态更新机制
在上一章节中,我们在设置 spring.config.import 参数时,指定了 refreshEnabled=true 参数,该参数表示是否需要动态监听远端 NacosServer 中该配置的变化,如果不指定该参数,SCA 只会在启动时加载一次配置,并不会在运行期监听配置变化以及更新 NacosPropertySource 中的内容,SpringBean 中的属性值也就无法运行期更新。
可以按照上图图示中的数字顺序了解 Nacos 配置动态更新的机制,当 spring.config.import 配置中添加了 refreshEnabled=true 参数,SCA 就会在 Spring 容器初始化完成后对 Nacos 配置进行监听,时间点上和配置启动加载的时间点并不一致,配置初始化的时间点是在所有 Bean 初始化之前,而监听配置变更的时间点是在所有 Bean 完成初始化之后。
成功监听后,当我们在 Nacos 控制台对配置进行更新时,应用程序中的 NacosClient 会通知 SCA 配置发生变化,SCA 在接受到底层 Nacos 回调后会向 Spring 发布 RefreshEvent 事件,Spring 中的 ContextRefresher 会接受该事件,将最新的配置更新到 NacosPropertySource 中,更新 Enviroment 对象,并且发布 RefreshScopeRefreshedEvent 事件,对所有添加了 RefreshScope 注解以及 ConfigurationProperties 注解的 SpringBean 进行重新初始化,从未获取到最新的属性值。
以上流程中 spring.config.import 配置中的 refreshEnabled=true 参数决定了 SCA 是否会监听配置并在 Nacos 中配置的变化时更新 Enviroment,而在 Bean 中添加 RefreshScope 注解以及 ConfigurationProperties 直接决定了当 Enviroment 对象中的属性发生变化时刷新 Bean 中的属性值。
属性源的优先级
上面我们了解到 Value 注解可以读取环境变量,JVM,application.properties 中的配置,不同的属性源中的属性 key 可能重复,这种情况下,Spring 读取属性有一个优先级,如下图所示,优先级为 JVM 参数>环境变量>Nacos 配置(spring.config.import 参数引入属性源)>工程本地 application.properties。
Nacos 中设置的属性值会覆盖工程本地的属性文件,但是其优先级低于 JVM 和环境变量,如果在环境变量和 JVM 参数配置了相同的参数,Nacos 中的配置将不会生效。SCA 在实现配置动态加载遵循了 Spring Boot 官方推荐的属性源优先级顺序,参考:https://docs.spring.io/spring-boot/reference/features/external-config.html
此外,spring.config.import 参数可以指定多个属性源,不同的属性源之间通过逗号 "," 分隔,多个不同属性源之间,引入顺序靠前,优先级更低。
在 spring boot 2.4 之前的版本中,Spring 不支持通过 spring.config.import 指定外部属性源,SCA 内部提供了 spring.cloud.nacos.config.shared-configs 和spring.cloud.nacos.config.extension-configs 参数来指定多个 Nacos 配置属性源,在最新的 SCA 版本 2023.0.1.3 中已经废弃这两个参数,统一到标准的 spring.config.import 参数。此外,在低版本的 Spring 中,支持在 bootstrap.yml 文件中配置参数,该种用法也在新版本 Spring 中废弃,统一将参数配置 application.properties,我们建议对依赖低版本的应用代码进行升级,统一改造为标准的方法进行配置。
Nacos日志
Nacos 扮演了配置动态推送的核心功能,通过查看 Nacos 的启动及运行时日志,可以帮助大家更好地理解两者整合的内部原理,并且有助于大家自主排查配置中心的常见问题,Nacos 客户端的日志目录默认在 {user.home}/logs/nacos/目录下,其中 {user.home} 是应用进程运行所属用户的主目录,在 Linux 系统中,如果进程以 root 启动,日志默认在/root/logs/nacos/下,如果以 admin 用户启动,日志默认在/home/admin/logs/nacos/下。在 nacos 目录下,我们可以看到 remote.log,config.log,naming.log 三个日志文件,其中 remote.log 记录 Nacos 客户端和服务端的长连接相关的日志,naming.log 是服务管理相关日志,config.log 是我们需要核心关注的配置相关日志,其中记录着 Nacos 客户端和 Nacos 服务端交互的详细日志。以下是几个关键的日志:
- add-listener:表示应用程序监听了配置,包括 namespace,dataId,group 三元组,只有正常监听了配置,才能在配置变更时收到推送
- server-push:表示应用程序收到了服务端的配置变更推送事件。
- data-received:表示应用程序收到推送事件后向服务端查询到了配置内容,包括 namespace,dataId,group 三元组以及接受到的配置 md5,可以和 Nacos 控制台比对 md5 值来判断是否接受到正确的版本
- notify-listener:表示应用程序收到了更新后的配置内容,并且尝试将最新的配置内容回调给对应的监听器,比如通知 SCA 重新加载 Nacos 的配置并且更新 Spring 上下文。
- notify-ok:表示 Nacos 已经成功回调了监听器,监听器中的回调已经正常执行。
- notify-error:表示 Nacos 回调了监听器,但是监听器执行是抛出了异常,从业务视角,该种情况会表现配置更新没有效果,需要根据具体异常进行处理。
- notify-block-monitor:表示 Nacos 回调了监听器,但监听器执行超时,默认监听器执行超过 60s 时会打印该日志。
通过阅读 Nacos 的日志,可以排查在使用 Nacos 配置中心过程遇到的问题,比如通过日志判断应用程序是否连接到了正确的 Nacos 服务端地址,是否监听了正确的 namespace,dataId 以及 group,是否正常收到了变更推送以及监听器回调时是否存在异常报错以及阻塞超时的情况。
在启动和运行时我们也可以在 Nacos 的 config.log 日志中观察到 Nacos 和 KMS 交互的日志,以便于更好地排查遇到的问题,关于集成 KMS 实现配置加解密的原理,可以参考《Nacos 安全零信任实践》一文中 Nacos 存储安全一节。
结语
以上我们在 Spring Cloud 应用中结合 Nacos 实现了运行期配置动态更新的功能,以及在此基础上结合 KMS 在不改动代码的情况下对应用使用的敏感配置进行保护,解决将配置迁移到 Nacos 中可能存在的数据安全顾虑,并对其底层工作原理做了简单介绍。Nacos 作为广泛使用的配置中心,除了基础的配置实时动态更新的核心功能外,还支持配置监听查询(配置订阅节点查询),推送轨迹,标签灰度等进阶的提升易用性功能。
安全性方面,我们对普通业务配置和敏感配置进行了拆分,从应用程序侧解决了敏感数据泄漏的问题,除此之外,我们在 MSE 控制台中也会有针对不同账号设置细粒度的访问控制的功能,比如控制业务普通配置对应用开发开放访问,数据源秘钥等敏感数据只对运维人员开放,MSE Nacos 也可以支持该功能,对于访问控制部分,我们后续也会有文章进行单独介绍,可关注后续文章。
相关链接:
[1] Nacos 官网
[2] Nacos Github 主仓库
https://github.com/alibaba/nacos
[3] 生态组仓库
https://github.com/nacos-group
[4] Spring Cloud Alibaba
https://sca.aliyun.com/docs/2023/user-guide/nacos/quick-start/
Nacos 多语言生态仓库:
[1] Nacos-GO-SDK
https://github.com/nacos-group/nacos-sdk-go
[2] Nacos-Python-SDK
https://github.com/nacos-group/nacos-sdk-python
[3] Nacos-Rust-SDK
https://github.com/nacos-group/nacos-sdk-rust
[4] Nacos C# SDK
https://github.com/nacos-group/nacos-sdk-csharp
[5] Nacos C++ SDK
https://github.com/nacos-group/nacos-sdk-cpp
[6] Nacos PHP-SDK
https://github.com/nacos-group/nacos-sdk-php
[7] Rust Nacos Server
https://github.com/nacos-group/r-nacos
推荐阅读:《MSE Nacos:解决敏感配置的安全隐患》