.NET Core结合Nacos实现配置加解密的方法

.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 里面拿一些额外的信息。

getordergetfiltername 属于辅助信息,指定这个 configfilter 的执行顺序(越小越先执行)和名称。

dofilter 就是核心了,它可以变更 request 和 response ,这两个对象内部都会维护一个包含配置信息的 dictionary。

换言之,只要我们定义一个 configfilter,实现了这个接口,那么配置想怎么操作都可以了,加解密就是小问题了。

其中 nacossdkoptions 里面加了两个配置项,是专门给这个功能用的 configfilterassembliesconfigfilterextinfo

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配置加解密内容请搜索硕编程以前的文章,希望大家多多支持硕编程

下一节:asp.net core为ihttpclientfactory添加动态命名配置

asp.net编程技术

相关文章