.NET Core結合Nacos實現配置加解密的方法
背景
當我們把應用的配置都放到配置中心後,很多人會想到這樣一個問題,配置裡面有敏感的信息要怎麼處理呢?
信息既然敏感的話,那麼加個密就好瞭嘛,相信大部分人的第一感覺都是這個,確實這個是最簡單也是最合適的方法。
其實很多人都在關註這個問題,好比說,數據庫的連接字符串,調用第三方的密鑰等等這些信息,都是不太想讓很多人知道的。
那麼如果我們把配置放在 Nacos 瞭,我們可以怎麼操作呢?
想瞭想不外乎這麼幾種:
- 全部服務端搞定,客戶端隻管取;
- 全部客戶端搞定,服務端隻管存;
- 客戶端為主,服務端為輔,服務端存一些加解密需要的輔助信息即可。
有一個老哥已經在 issue 裡面提出瞭相關的落地方案,也包含瞭部分實現。
https://github.com/alibaba/nacos/issues/5367
簡要概述的話就是,開個口子,用戶可以在客戶端拓展任意加解密方式,同時服務端可以輔助這一操作。
不過看瞭 2.0.2 的代碼,服務端這一塊的“輔助”還未完成,不過對客戶端來說,這一塊其實問題已經不大瞭。
6月14號發佈的 nacos-sdk-csharp
1.1.0 版本已經支持瞭這一功能
下面就用 .NET 5 和 Nacos 2.0.2 為例,來簡單說明一下。
簡單原理說明
sdk 裡面在進行配置相關讀寫操作的時候,會有一個 DoFilter
的操作。這個操作就是我們的切入點。
既然要執行 Filter , 那麼執行的 Filter 從那裡來呢? 答案是 IConfigFilter
。
sdk 裡面提供瞭 IConfigFilter
這個接口,但是不提供實現,具體實現交由用戶自定義,畢竟 100 個人就有 100 種不一樣的實現。
下面看看它的定義。
public interface IConfigFilter { void Init(NacosSdkOptions options); int GetOrder(); string GetFilterName(); void DoFilter(IConfigRequest request, IConfigResponse response, IConfigFilterChain filterChain); }
Init
方法就是對這個 ConfigFilter 進行一些初始化操作,好比說從 Options 裡面拿一些額外的信息。
GetOrder
和 GetFilterName
屬於輔助信息,指定這個 ConfigFilter 的執行順序(越小越先執行)和名稱。
DoFilter
就是核心瞭,它可以變更 request 和 response ,這兩個對象內部都會維護一個包含配置信息的 Dictionary。
換言之,隻要我們定義一個 ConfigFilter,實現瞭這個接口,那麼配置想怎麼操作都可以瞭,加解密就是小問題瞭。
其中 NacosSdkOptions 裡面加瞭兩個配置項,是專門給這個功能用的 ConfigFilterAssemblies
和 ConfigFilterExtInfo
ConfigFilterAssemblies
是自定義 ConfigFilter 所在的程序集的名字,這裡是一個字符串列表類型的參數,sdk 會根據這個名字去找到對應的實現,然後初始化好。
ConfigFilterExtInfo
是實現 ConfigFilter 是需要用到的擴展信息,這裡是一個字符串類型的參數,擴展信息復雜的可以考慮傳入一個 JSON 字符串。
下面來看個具體的例子吧。
自定義 ConfigFilter
這個 Filter 實現的效果是把部分敏感配置項進行加密,敏感的配置項需要在配置文件中指定。
先是 Init
方法:
public void Init(NacosSdkOptions options) { // 從 Options 裡面的拓展信息獲取需要加密的 json path // 這裡隻是示例,根據具體情況調整成自己合適的!!!! var extInfo = JObject.Parse(options.ConfigFilterExtInfo); if (extInfo.ContainsKey("JsonPaths")) { // JsonPaths 在這裡的含義是,那個path下面的內容要加密 _jsonPaths = extInfo.GetValue("JsonPaths").ToObject<List<string>>(); } }
然後是 DoFilter
方法:
這個方法裡面要註意幾點:
- request 隻有請求的時候才會有值,其他時候都是 null 值。
- response 隻有響應的時候才會有值,其他時候都是 null 值。
- 操作完之後,一定要調用 PutParameter 方法進行覆蓋才會生效。
public void DoFilter(IConfigRequest request, IConfigResponse response, IConfigFilterChain filterChain) { if (request != null) { var encryptedDataKey = DefaultKey; var raw_content = request.GetParameter(Nacos.V2.Config.ConfigConstants.CONTENT); // 部分配置加密後的 content var content = ReplaceJsonNode((string)raw_content, encryptedDataKey, true); // 加密配置後,不要忘記更新 request !!!! request.PutParameter(Nacos.V2.Config.ConfigConstants.ENCRYPTED_DATA_KEY, encryptedDataKey); request.PutParameter(Nacos.V2.Config.ConfigConstants.CONTENT, content); } if (response != null) { var resp_content = response.GetParameter(Nacos.V2.Config.ConfigConstants.CONTENT); var resp_encryptedDataKey = response.GetParameter(Nacos.V2.Config.ConfigConstants.ENCRYPTED_DATA_KEY); // nacos 2.0.2 服務端目前還沒有把 encryptedDataKey 記錄並返回,所以 resp_encryptedDataKey 目前隻會是 null // 如果服務端有記錄並且能返回,我們可以做到每一個配置都用不一樣的 encryptedDataKey 來加解密。 // 目前的話,隻能固定一個 encryptedDataKey var encryptedDataKey = (resp_encryptedDataKey == null || string.IsNullOrWhiteSpace((string)resp_encryptedDataKey)) ? DefaultKey : (string)resp_encryptedDataKey; var content = ReplaceJsonNode((string)resp_content, encryptedDataKey, false); response.PutParameter(Nacos.V2.Config.ConfigConstants.CONTENT, content); } }
這裡涉及 encryptedDataKey
的相關操作都隻是預留操作,現階段可以不用理會。
還有一個 ReplaceJsonNode
方法就是替換敏感配置的具體操作瞭。
private string ReplaceJsonNode(string src, string encryptedDataKey, bool isEnc = true) { // 示例配置用的是JSON,如果用的是 yaml,這裡換成用 yaml 解析即可。 var jObj = JObject.Parse(src); foreach (var item in _jsonPaths) { var t = jObj.SelectToken(item); if (t != null) { var r = t.ToString(); // 加解密 var newToken = isEnc ? AESEncrypt(r, encryptedDataKey) : AESDecrypt(r, encryptedDataKey); if (!string.IsNullOrWhiteSpace(newToken)) { // 替換舊值 t.Replace(newToken); } } } return jObj.ToString(); }
到這裡,自定義的 ConfigFilter 已經完成瞭,下面就是真正的應用瞭。
簡單應用
老樣子,建一個 WebApi 項目,添加自定義 ConfigFilter 所在的包/項目/程序集。
這裡用的是集成 ASP.NET Core 的例子。
修改 appsettings.json
{ "NacosConfig": { "Listeners": [ { "Optional": true, "DataId": "demo", "Group": "DEFAULT_GROUP" } ], "Namespace": "cs", "ServerAddresses": [ "http://localhost:8848/" ], "ConfigFilterAssemblies": [ "XXXX.CusLib" ], "ConfigFilterExtInfo": "{\"JsonPaths\":[\"ConnectionStrings.Default\"],\"Other\":\"xxxxxx\"}" } }
註:老黃這裡把 Optional 設置成 true,是為瞭第一次運行的時候,如果服務端沒有進行配置而不至於退出程序。
修改 Program.cs
public class Program { public static void Main(string[] args) { var outputTemplate = "{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{Level}] {Message}{NewLine}{Exception}"; Log.Logger = new LoggerConfiguration() .Enrich.FromLogContext() .MinimumLevel.Override("Microsoft", LogEventLevel.Warning) .MinimumLevel.Override("System", LogEventLevel.Warning) .MinimumLevel.Debug() .WriteTo.Console(outputTemplate: outputTemplate) .CreateLogger(); System.Text.Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance); try { Log.ForContext<Program>().Information("Application starting..."); CreateHostBuilder(args, Log.Logger).Build().Run(); } catch (System.Exception ex) { Log.ForContext<Program>().Fatal(ex, "Application start-up failed!!"); } finally { Log.CloseAndFlush(); } } public static IHostBuilder CreateHostBuilder(string[] args, Serilog.ILogger logger) => Host.CreateDefaultBuilder(args) .ConfigureAppConfiguration((context, builder) => { var c = builder.Build(); builder.AddNacosV2Configuration(c.GetSection("NacosConfig"), logAction: x => x.AddSerilog(logger)); }) .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>().UseUrls("http://*:8787"); }) .UseSerilog(); }
最後是 Startup.cs
public class Startup { // 省略部分.... public void ConfigureServices(IServiceCollection services) { services.AddNacosV2Config(Configuration, null, "NacosConfig"); services.Configure<AppSettings>(Configuration.GetSection("AppSettings")); services.AddControllers(); } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { var configSvc = app.ApplicationServices.GetRequiredService<Nacos.V2.INacosConfigService>(); var db = $"demo-{DateTimeOffset.Now.ToString("yyyyMMdd_HHmmss")}"; var oldConfig = "{\"ConnectionStrings\":{\"Default\":\"Server=127.0.0.1;Port=3306;Database=" + db + ";User Id=app;Password=098765;\"},\"version\":\"測試version---\",\"AppSettings\":{\"Str\":\"val\",\"num\":100,\"arr\":[1,2,3,4,5],\"subobj\":{\"a\":\"" + db + "\"}}}"; configSvc.PublishConfig("demo", "DEFAULT_GROUP", oldConfig).ConfigureAwait(false).GetAwaiter().GetResult(); var options = app.ApplicationServices.GetRequiredService<IOptionsMonitor<AppSettings>>(); Console.WriteLine("===用 IOptionsMonitor 讀取配置==="); Console.WriteLine(Newtonsoft.Json.JsonConvert.SerializeObject(options.CurrentValue)); Console.WriteLine(""); Console.WriteLine("===用 IConfiguration 讀取配置==="); Console.WriteLine(Configuration["ConnectionStrings:Default"]); Console.WriteLine(""); var pwd = $"demo-{new Random().Next(100000, 999999)}"; var newConfig = "{\"ConnectionStrings\":{\"Default\":\"Server=127.0.0.1;Port=3306;Database="+ db + ";User Id=app;Password="+ pwd +";\"},\"version\":\"測試version---\",\"AppSettings\":{\"Str\":\"val\",\"num\":100,\"arr\":[1,2,3,4,5],\"subobj\":{\"a\":\""+ db +"\"}}}"; // 模擬 配置變更 configSvc.PublishConfig("demo", "DEFAULT_GROUP", newConfig).ConfigureAwait(false).GetAwaiter().GetResult(); System.Threading.Thread.Sleep(500); var options2 = app.ApplicationServices.GetRequiredService<IOptionsMonitor<AppSettings>>(); Console.WriteLine("===用 IOptionsMonitor 讀取配置==="); Console.WriteLine(Newtonsoft.Json.JsonConvert.SerializeObject(options2.CurrentValue)); Console.WriteLine(""); Console.WriteLine("===用 IConfiguration 讀取配置==="); Console.WriteLine(Configuration["ConnectionStrings:Default"]); Console.WriteLine(""); // 省略部分.... } }
最後來看看幾張效果圖:
首先是程序的運行日志。
其次是和 Nacos 控制臺的對比。
到這裡的話,基於 Nacos 的加解密就完成瞭。
寫在最後
敏感配置項的加解密還是很有必要的,配置中心負責存儲,客戶端負責加解密,這樣的方式可以讓用戶更加靈活的選擇自己想要的加解密方法。
本文的示例代碼已經上傳到 Github,僅供參考。
https://github.com/catcherwong-archive/2021/tree/main/NacosConfigWithEncryption
最後的最後,希望感興趣的大佬可以一起參與到這個項目來。
nacos-sdk-csharp 的地址 :https://github.com/nacos-group/nacos-sdk-csharp
到此這篇關於.NET Core結合Nacos實現配置加解密的方法的文章就介紹到這瞭,更多相關.NET Core Nacos配置加解密內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- ASP.NET Core中的配置詳解
- 為什麼ASP.NET Core 數據庫連接串的值和appsettings.json配的不一樣?
- ASP.Net Core MVC基礎系列之環境設置
- .NET5控制臺程序使用EF連接MYSQL數據庫的方法
- ASP.NET Core配置設置之Configuration包