聊一聊.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 方法進行覆蓋才會生效。
這里涉及 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实现配置加解密的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: WPF 用Popup做下拉菜单
- 下一篇: NET问答: 是否有通用的方法判断一个