作者:柳遵飛
前言
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。
- 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/
- 初始化 Nacos 配置
可以選擇開源 nacos 或者購買商業化 MSE Nacos 版本,以下的圖示為商業化 Nacos。
假設我們的應用為支付業務的應用,應用名為 pay。
在 Nacos 例項中可以建立 dataId=pay-application.properties,group=core 的配置。
- 修改應用工程中的 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 引數。
- 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 傳輸過程中資料是否存在洩漏可能性,敏感配置和普通的業務配置能否設定不同的讀寫許可權,要實現以上安全性的要求,業務的程式碼是否可以儘量低成本改造等等。
要實現以上的安全性訴求,我們要做到以下幾點:
- 敏感配置在 Nacos 需要加密儲存,不能直接明文儲存2. 敏感配置在傳輸過程中需要加密傳輸,防止中間網路裝置透過抓包方式竊取資料。3. 應用中的業務程式碼不能感知配置是否加密,仍需要按照之前的方式讀取屬性值,降低改造成本。
以下我們將介紹如何透過整合 KMS 實現零程式碼改造實現上述訴求。
加密配置拆分
如果我們期望將普通業務配置和敏感配置分離,我們可以 dataId=pay-application.properties,group=core 的配置進行拆分,將敏感資料單獨拆分出一個獨立的加密 Nacos 配置,比如 dataId=cipher-kms-aes-256-pay-application.properties,group=secret,用於存放資料來源 token 等相關的敏感配置。為了防止解密配置和普通配置屬性檔案中的屬性值重複,我們可以在加密配置中的屬性值統一加上 encrypted 字首。
Nacos 中的配置
- 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
- dataId=pay-application.properties,group=core
原先的 pay-application.properties 中的屬性暫時保持不動,等應用程式側的所有節點引入新配置 cipher-kms-aes-256-pay-application.properties 之後,再對其做變更。
工程內配置改造
- 引入 MSE 外掛擴充套件包
加密的配置在儲存和網路傳輸過程中都是密文,因此需要在應用側支援解密的功能,在 pom 中引入解密外掛。
<dependency>
<groupId>com.alibaba.nacos</groupId>
<artifactId>nacos-client-mse-extension</artifactId>
<version>1.0.4</version>
</dependency>
- 調整專案工程下的 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-encr...
修改配置重啟業務應用完成後,此時應用程式讀取的還是 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/externa...
此外,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 官網
https://nacos.io
[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