欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Asp.Net Core Identity 隐私数据保护的实现

程序员文章站 2022-03-07 13:14:30
前言 asp.net core identity 是asp.net core 的重要组成部分,他为 asp.net core 甚至其他 .net core 应用程序提供了一个简单易用...

前言

asp.net core identity 是asp.net core 的重要组成部分,他为 asp.net core 甚至其他 .net core 应用程序提供了一个简单易用且易于扩展的基础用户管理系统框架。它包含了基本的用户、角色、第三方登录、claim等功能,使用 identity server 4 可以为其轻松扩展 openid connection 和 oauth 2.0 相关功能。网上已经有大量相关文章介绍,不过这还不是 asp.net core identity 的全部,其中一个就是隐私数据保护。

正文

乍一看,隐私数据保护是个什么东西,感觉好像知道,但又说不清楚。确实这个东西光说很难解释清楚,那就直接上图:

Asp.Net Core Identity 隐私数据保护的实现

这是用户表的一部分,有没有发现问题所在?用户名和 email 字段变成了一堆看不懂的东西。仔细看会发现这串乱码好像还有点规律:guid + 冒号 +貌似是 base64 编码的字符串,当然这串字符串去在线解码结果还是一堆乱码,比如 id 为 1 的 username :svbqhhluyzsipzvuf4baoq== 在线解码后是²ðj†na”¢=•t†ú9 。

这就是隐私数据保护,如果没有这个功能,那么用户名是明文存储的,虽然密码依然是hash难以破解,但如果被拖库,用户数据也会面临更大的风险。因为很多人喜欢在不同的网站使用相同的账号信息进行注册,避免遗忘。如果某个网站的密码被盗,其他网站被拖库,黑客就可以比对是否有相同的用户名,尝试撞库,甚至如果 email 被盗,黑客还可以看着 email 用找回密码把账号给 ntr 了。而隐私数据保护就是一层更坚实的后盾,哪怕被拖库,黑客依然看不懂里面的东西。

然后是这个格式,基本能想到,冒号应该是分隔符,前面一个 guid,后面是加密后的内容。那问题就变成了 guid 又是干嘛的?直接把加密的内容存进去不就完了。这其实是微软开发框架注重细节的最佳体现,接下来结合代码就能一探究竟。

启用隐私数据保护

 //注册identity服务(使用ef存储,在ef上下文之后注册)
services.addidentity<applicationuser, applicationrole>(options =>
{
 //...
 options.stores.protectpersonaldata = true; //在这里启用隐私数据保护
})
//...
.addpersonaldataprotection<aesprotector, aesprotectorkeyring>(); //在这里配置数据加密器,一旦启用保护,这里必须配置,否则抛出异常

其中的aesprotector 和aesprotectorkeyring 需要自行实现,微软并没有提供现成的类,至少我没有找到,估计也是这个功能冷门的原因吧。.neter 都被微软给惯坏了,都是衣来伸手饭来张口。有没有发现aesprotectorkeyring 中有keyring 字样?钥匙串,恭喜你猜对了,guid 就是这个钥匙串中一把钥匙的编号。也就是说如果加密的钥匙被盗,但不是全部被盗,那用户信息还不会全部泄露。微软这一手可真是狠啊!

接下来看看这两个类是什么吧。

aesprotector 是 ilookupprotector 的实现。接口包含两个方法,分别用于加密和解密,返回字符串,参数包含字符串数据和上面那个 guid,当然实际只要是字符串就行, guid 是我个人的选择,生成不重复字符串还是 guid 方便。

aesprotectorkeyring 则是 ilookupprotectorkeyring 的实现。接口包含1、获取当前正在使用的钥匙编号的只读属性,用于提供加密钥匙;2、根据钥匙编号获取字符串的索引器(我这里就是原样返回的。。。);3、获取所有钥匙编号的方法。

aesprotector

 class aesprotector : ilookupprotector
  {
    private readonly object _locker;

    private readonly dictionary<string, securityutil.aesprotector> _protectors;

    private readonly directoryinfo _dirinfo;

    public aesprotector(iwebhostenvironment environment)
    {
      _locker = new object();

      _protectors = new dictionary<string, securityutil.aesprotector>();

      _dirinfo = new directoryinfo($@"{environment.contentrootpath}\app_data\aesdataprotectionkey");
    }

    public string protect(string keyid, string data)
    {
      if (data.isnullorempty())
      {
        return data;
      }

      checkorcreateprotector(keyid);

      return _protectors[keyid].protect(encoding.utf8.getbytes(data)).tobase64string();
    }

    public string unprotect(string keyid, string data)
    {
      if (data.isnullorempty())
      {
        return data;
      }

      checkorcreateprotector(keyid);

      return encoding.utf8.getstring(_protectors[keyid].unprotect(data.tobytesfrombase64string()));
    }

    private void checkorcreateprotector(string keyid)
    {
      if (!_protectors.containskey(keyid))
      {
        lock (_locker)
        {
          if (!_protectors.containskey(keyid))
          {
            var fileinfo = _dirinfo.getfiles().firstordefault(d => d.name == $@"key-{keyid}.xml") ??
                    throw new filenotfoundexception();
            using (var stream = fileinfo.openread())
            {
              xdocument xmldoc = xdocument.load(stream);
              _protectors.add(keyid,
                new securityutil.aesprotector(xmldoc.element("key")?.element("encryption")?.element("masterkey")?.value.tobytesfrombase64string()
                  , xmldoc.element("key")?.element("encryption")?.element("iv")?.value.tobytesfrombase64string()
                  , int.parse(xmldoc.element("key")?.element("encryption")?.attribute("blocksize")?.value)
                  , int.parse(xmldoc.element("key")?.element("encryption")?.attribute("keysize")?.value)
                  , int.parse(xmldoc.element("key")?.element("encryption")?.attribute("feedbacksize")?.value)
                  , enum.parse<paddingmode>(xmldoc.element("key")?.element("encryption")?.attribute("padding")?.value)
                  , enum.parse<ciphermode>(xmldoc.element("key")?.element("encryption")?.attribute("mode")?.value)));
            }
          }
        }
      }
    }
  }

aesprotectorkeyring

class aesprotectorkeyring : ilookupprotectorkeyring
  {
    private readonly object _locker;
    private readonly dictionary<string, xdocument> _keyrings;
    private readonly directoryinfo _dirinfo;

    public aesprotectorkeyring(iwebhostenvironment environment)
    {
      _locker = new object();
      _keyrings = new dictionary<string, xdocument>();
      _dirinfo = new directoryinfo($@"{environment.contentrootpath}\app_data\aesdataprotectionkey");

      readkeys(_dirinfo);
    }

    public ienumerable<string> getallkeyids()
    {
      return _keyrings.keys;
    }

    public string currentkeyid => newestactivationkey(datetimeoffset.now)?.element("key")?.attribute("id")?.value ?? generatekey(_dirinfo)?.element("key")?.attribute("id")?.value;

    public string this[string keyid] =>
      getallkeyids().firstordefault(id => id == keyid) ?? throw new keynotfoundexception();

    private void readkeys(directoryinfo dirinfo)
    {
      foreach (var fileinfo in dirinfo.getfiles().where(f => f.extension == ".xml"))
      {
        using (var stream = fileinfo.openread())
        {
          xdocument xmldoc = xdocument.load(stream);

          _keyrings.tryadd(xmldoc.element("key")?.attribute("id")?.value, xmldoc);
        }
      }
    }

    private xdocument generatekey(directoryinfo dirinfo)
    {
      var now = datetimeoffset.now;
      if (!_keyrings.any(item =>
        datetimeoffset.parse(item.value.element("key")?.element("activationdate")?.value) <= now
        && datetimeoffset.parse(item.value.element("key")?.element("expirationdate")?.value) > now))
      {
        lock (_locker)
        {
          if (!_keyrings.any(item =>
            datetimeoffset.parse(item.value.element("key")?.element("activationdate")?.value) <= now
            && datetimeoffset.parse(item.value.element("key")?.element("expirationdate")?.value) > now))
          {
            var masterkeyid = guid.newguid().tostring();

            xdocument xmldoc = new xdocument();
            xmldoc.declaration = new xdeclaration("1.0", "utf-8", "yes");

            xelement key = new xelement("key");
            key.setattributevalue("id", masterkeyid);
            key.setattributevalue("version", 1);

            xelement creationdate = new xelement("creationdate");
            creationdate.setvalue(now);

            xelement activationdate = new xelement("activationdate");
            activationdate.setvalue(now);

            xelement expirationdate = new xelement("expirationdate");
            expirationdate.setvalue(now.adddays(90));

            xelement encryption = new xelement("encryption");
            encryption.setattributevalue("blocksize", 128);
            encryption.setattributevalue("keysize", 256);
            encryption.setattributevalue("feedbacksize", 128);
            encryption.setattributevalue("padding", paddingmode.pkcs7);
            encryption.setattributevalue("mode", ciphermode.cbc);

            securityutil.aesprotector protector = new securityutil.aesprotector();
            xelement masterkey = new xelement("masterkey");
            masterkey.setvalue(protector.generatekey().tobase64string());

            xelement iv = new xelement("iv");
            iv.setvalue(protector.generateiv().tobase64string());

            xmldoc.add(key);
            key.add(creationdate);
            key.add(activationdate);
            key.add(expirationdate);
            key.add(encryption);
            encryption.add(masterkey);
            encryption.add(iv);

            xmldoc.save(
              $@"{dirinfo.fullname}\key-{masterkeyid}.xml");

            _keyrings.add(masterkeyid, xmldoc);

            return xmldoc;
          }

          return newestactivationkey(now);
        }
      }

      return newestactivationkey(now);
    }

    private xdocument newestactivationkey(datetimeoffset now)
    {
      return _keyrings.where(item =>
          datetimeoffset.parse(item.value.element("key")?.element("activationdate")?.value) <= now
          && datetimeoffset.parse(item.value.element("key")?.element("expirationdate")?.value) > now)
        .orderbydescending(item =>
          datetimeoffset.parse(item.value.element("key")?.element("expirationdate")?.value)).firstordefault().value;
    }
  }

这两个类也是注册到 asp.net core di 中的服务,所有 di 的功能都支持。

在其中我还使用了我在其他地方写的底层基础工具类,如果想看完整实现可以去我的 github 克隆代码实际运行并体验。在这里大致说一下这两个类的设计思路。既然微软设计了钥匙串功能,那自然是要利用好。我在代码里写死每个钥匙有效期90天,过期后会自动生成并使用新的钥匙,钥匙的详细信息使用xml文档保存在项目文件夹中,具体见下面的截图。identity 会使用最新钥匙进行加密并把钥匙编号一并存入数据库,在读取时会根据编号找到对应的加密器解密数据。这个过程由 ef core 的值转换器(ef core 2.1 增加)完成,也就是说 identity 向 dbcontext 中需要加密的字段注册了值转换器。所以我也不清楚早期 identity 有没有这个功能,不使用 ef core 的情况下这个功能是否可用。

如果希望对自定义用户数据进行保护,为对应属性标注 [personaldata] 特性即可。identity 已经对内部的部分属性进行了标记,比如上面提到的 username 。

有几个要特别注意的点:

1、在有数据的情况下不要随便开启或关闭数据保护功能,否则可能导致严重后果。

2、钥匙一定要保护好,保存好。否则可能泄露用户数据或者再也无法解密用户数据,从删库到跑路那种 shift + del 的事千万别干。

3、被保护的字段无法在数据库端执行模糊搜索,只能精确匹配。如果希望进行数据分析,只能先用 identity 把数据读取到内存才能继续做其他事。

4、钥匙的有效期不宜过短,因为在用户登录时 identity 并不知道用户是什么时候注册的,应该用哪个钥匙,所以 identity 会用所有钥匙加密一遍然后查找是否有精确匹配的记录。钥匙的有效期越短,随着网站运行时间的增加,钥匙数量会增加,要尝试的钥匙也会跟着增加,最后对系统性能产生影响。当然这可以用缓存来缓解。

效果预览:

Asp.Net Core Identity 隐私数据保护的实现

Asp.Net Core Identity 隐私数据保护的实现

  本文地址:https://www.cnblogs.com/coredx/p/12210232.html

  完整源代码:github

  里面有各种小东西,这只是其中之一,不嫌弃的话可以star一下。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。