.net core結(jié)合nacos實(shí)現(xiàn)配置加解密的方法
背景
當(dāng)我們把應(yīng)用的配置都放到配置中心后,很多人會(huì)想到這樣一個(gè)問題,配置里面有敏感的信息要怎么處理呢?
信息既然敏感的話,那么加個(gè)密就好了嘛,相信大部分人的第一感覺都是這個(gè),確實(shí)這個(gè)是最簡單也是最合適的方法。
其實(shí)很多人都在關(guān)注這個(gè)問題,好比說,數(shù)據(jù)庫的連接字符串,調(diào)用第三方的密鑰等等這些信息,都是不太想讓很多人知道的。
那么如果我們把配置放在 nacos 了,我們可以怎么操作呢?
想了想不外乎這么幾種:
- 全部服務(wù)端搞定,客戶端只管??;
- 全部客戶端搞定,服務(wù)端只管存;
- 客戶端為主,服務(wù)端為輔,服務(wù)端存一些加解密需要的輔助信息即可。
有一個(gè)老哥已經(jīng)在 issue 里面提出了相關(guān)的落地方案,也包含了部分實(shí)現(xiàn)。
https://github.com/alibaba/nacos/issues/5367
簡要概述的話就是,開個(gè)口子,用戶可以在客戶端拓展任意加解密方式,同時(shí)服務(wù)端可以輔助這一操作。
不過看了 2.0.2 的代碼,服務(wù)端這一塊的“輔助”還未完成,不過對客戶端來說,這一塊其實(shí)問題已經(jīng)不大了。
6月14號發(fā)布的 nacos-sdk-csharp
1.1.0 版本已經(jīng)支持了這一功能
下面就用 .net 5 和 nacos 2.0.2 為例,來簡單說明一下。
簡單原理說明
sdk 里面在進(jìn)行配置相關(guān)讀寫操作的時(shí)候,會(huì)有一個(gè) dofilter
的操作。這個(gè)操作就是我們的切入點(diǎn)。
既然要執(zhí)行 filter , 那么執(zhí)行的 filter 從那里來呢? 答案是 iconfigfilter
。
sdk 里面提供了 iconfigfilter
這個(gè)接口,但是不提供實(shí)現(xiàn),具體實(shí)現(xiàn)交由用戶自定義,畢竟 100 個(gè)人就有 100 種不一樣的實(shí)現(xiàn)。
下面看看它的定義。
public interface iconfigfilter { void init(nacossdkoptions options); int getorder(); string getfiltername(); void dofilter(iconfigrequest request, iconfigresponse response, iconfigfilterchain filterchain); }
init
方法就是對這個(gè) configfilter 進(jìn)行一些初始化操作,好比說從 options 里面拿一些額外的信息。
getorder
和 getfiltername
屬于輔助信息,指定這個(gè) configfilter 的執(zhí)行順序(越小越先執(zhí)行)和名稱。
dofilter
就是核心了,它可以變更 request 和 response ,這兩個(gè)對象內(nèi)部都會(huì)維護(hù)一個(gè)包含配置信息的 dictionary。
換言之,只要我們定義一個(gè) configfilter,實(shí)現(xiàn)了這個(gè)接口,那么配置想怎么操作都可以了,加解密就是小問題了。
其中 nacossdkoptions 里面加了兩個(gè)配置項(xiàng),是專門給這個(gè)功能用的 configfilterassemblies
和 configfilterextinfo
configfilterassemblies
是自定義 configfilter 所在的程序集的名字,這里是一個(gè)字符串列表類型的參數(shù),sdk 會(huì)根據(jù)這個(gè)名字去找到對應(yīng)的實(shí)現(xiàn),然后初始化好。
configfilterextinfo
是實(shí)現(xiàn) configfilter 是需要用到的擴(kuò)展信息,這里是一個(gè)字符串類型的參數(shù),擴(kuò)展信息復(fù)雜的可以考慮傳入一個(gè) json 字符串。
下面來看個(gè)具體的例子吧。
自定義 configfilter
這個(gè) filter 實(shí)現(xiàn)的效果是把部分敏感配置項(xiàng)進(jìn)行加密,敏感的配置項(xiàng)需要在配置文件中指定。
先是 init
方法:
public void init(nacossdkoptions options) { // 從 options 里面的拓展信息獲取需要加密的 json path // 這里只是示例,根據(jù)具體情況調(diào)整成自己合適的!?。?! var extinfo = jobject.parse(options.configfilterextinfo); if (extinfo.containskey("jsonpaths")) { // jsonpaths 在這里的含義是,那個(gè)path下面的內(nèi)容要加密 _jsonpaths = extinfo.getvalue("jsonpaths").toobject<list<string>>(); } }
然后是 dofilter
方法:
這個(gè)方法里面要注意幾點(diǎn):
- request 只有請求的時(shí)候才會(huì)有值,其他時(shí)候都是 null 值。
- response 只有響應(yīng)的時(shí)候才會(huì)有值,其他時(shí)候都是 null 值。
- 操作完之后,一定要調(diào)用 putparameter 方法進(jìn)行覆蓋才會(huì)生效。
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 服務(wù)端目前還沒有把 encrypteddatakey 記錄并返回,所以 resp_encrypteddatakey 目前只會(huì)是 null // 如果服務(wù)端有記錄并且能返回,我們可以做到每一個(gè)配置都用不一樣的 encrypteddatakey 來加解密。 // 目前的話,只能固定一個(gè) 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
的相關(guān)操作都只是預(yù)留操作,現(xiàn)階段可以不用理會(huì)。
還有一個(gè) 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 已經(jīng)完成了,下面就是真正的應(yīng)用了。
簡單應(yīng)用
老樣子,建一個(gè) webapi 項(xiàng)目,添加自定義 configfilter 所在的包/項(xiàng)目/程序集。
這里用的是集成 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 設(shè)置成 true,是為了第一次運(yùn)行的時(shí)候,如果服務(wù)端沒有進(jìn)行配置而不至于退出程序。
修改 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(""); // 省略部分.... } }
最后來看看幾張效果圖:
首先是程序的運(yùn)行日志。
其次是和 nacos 控制臺(tái)的對比。
到這里的話,基于 nacos 的加解密就完成了。
寫在最后
敏感配置項(xiàng)的加解密還是很有必要的,配置中心負(fù)責(zé)存儲(chǔ),客戶端負(fù)責(zé)加解密,這樣的方式可以讓用戶更加靈活的選擇自己想要的加解密方法。
本文的示例代碼已經(jīng)上傳到 github,僅供參考。
https://github.com/catcherwong-archive/2021/tree/main/nacosconfigwithencryption
最后的最后,希望感興趣的大佬可以一起參與到這個(gè)項(xiàng)目來。
nacos-sdk-csharp 的地址 :https://github.com/nacos-group/nacos-sdk-csharp
關(guān)于.net core結(jié)合nacos實(shí)現(xiàn)配置加解密的方法的文章就介紹至此,更多相關(guān).net core nacos配置加解密內(nèi)容請搜索碩編程以前的文章,希望大家多多支持碩編程!