• Asp.Net Core Identity 隐私数据保护


    前言

           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 的全部,其中一个就是隐私数据保护。

    正文

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

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

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

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

           启用隐私数据保护

    1 //注册Identity服务(使用EF存储,在EF上下文之后注册)
    2 services.AddIdentity<ApplicationUser, ApplicationRole>(options =>
    3 {
    4   //...
    5   options.Stores.ProtectPersonalData = true; //在这里启用隐私数据保护
    6 })
    7 //...
    8 .AddPersonalDataProtection<AesProtector, AesProtectorKeyRing>(); //在这里配置数据加密器,一旦启用保护,这里必须配置,否则抛出异常

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

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

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

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

           AesProtector

     1     class AesProtector : ILookupProtector
     2     {
     3         private readonly object _locker;
     4 
     5         private readonly Dictionary<string, SecurityUtil.AesProtector> _protectors;
     6 
     7         private readonly DirectoryInfo _dirInfo;
     8 
     9         public AesProtector(IWebHostEnvironment environment)
    10         {
    11             _locker = new object();
    12 
    13             _protectors = new Dictionary<string, SecurityUtil.AesProtector>();
    14 
    15             _dirInfo = new DirectoryInfo($@"{environment.ContentRootPath}App_DataAesDataProtectionKey");
    16         }
    17 
    18         public string Protect(string keyId, string data)
    19         {
    20             if (data.IsNullOrEmpty())
    21             {
    22                 return data;
    23             }
    24 
    25             CheckOrCreateProtector(keyId);
    26 
    27             return _protectors[keyId].Protect(Encoding.UTF8.GetBytes(data)).ToBase64String();
    28         }
    29 
    30         public string Unprotect(string keyId, string data)
    31         {
    32             if (data.IsNullOrEmpty())
    33             {
    34                 return data;
    35             }
    36 
    37             CheckOrCreateProtector(keyId);
    38 
    39             return Encoding.UTF8.GetString(_protectors[keyId].Unprotect(data.ToBytesFromBase64String()));
    40         }
    41 
    42         private void CheckOrCreateProtector(string keyId)
    43         {
    44             if (!_protectors.ContainsKey(keyId))
    45             {
    46                 lock (_locker)
    47                 {
    48                     if (!_protectors.ContainsKey(keyId))
    49                     {
    50                         var fileInfo = _dirInfo.GetFiles().FirstOrDefault(d => d.Name == $@"key-{keyId}.xml") ??
    51                                        throw new FileNotFoundException();
    52                         using (var stream = fileInfo.OpenRead())
    53                         {
    54                             XDocument xmlDoc = XDocument.Load(stream);
    55                             _protectors.Add(keyId,
    56                                 new SecurityUtil.AesProtector(xmlDoc.Element("key")?.Element("encryption")?.Element("masterKey")?.Value.ToBytesFromBase64String()
    57                                     , xmlDoc.Element("key")?.Element("encryption")?.Element("iv")?.Value.ToBytesFromBase64String()
    58                                     , int.Parse(xmlDoc.Element("key")?.Element("encryption")?.Attribute("BlockSize")?.Value)
    59                                     , int.Parse(xmlDoc.Element("key")?.Element("encryption")?.Attribute("KeySize")?.Value)
    60                                     , int.Parse(xmlDoc.Element("key")?.Element("encryption")?.Attribute("FeedbackSize")?.Value)
    61                                     , Enum.Parse<PaddingMode>(xmlDoc.Element("key")?.Element("encryption")?.Attribute("Padding")?.Value)
    62                                     , Enum.Parse<CipherMode>(xmlDoc.Element("key")?.Element("encryption")?.Attribute("Mode")?.Value)));
    63                         }
    64                     }
    65                 }
    66             }
    67         }
    68     }

           AesProtectorKeyRing

      1     class AesProtectorKeyRing : ILookupProtectorKeyRing
      2     {
      3         private readonly object _locker;
      4         private readonly Dictionary<string, XDocument> _keyRings;
      5         private readonly DirectoryInfo _dirInfo;
      6 
      7         public AesProtectorKeyRing(IWebHostEnvironment environment)
      8         {
      9             _locker = new object();
     10             _keyRings = new Dictionary<string, XDocument>();
     11             _dirInfo = new DirectoryInfo($@"{environment.ContentRootPath}App_DataAesDataProtectionKey");
     12 
     13             ReadKeys(_dirInfo);
     14         }
     15 
     16         public IEnumerable<string> GetAllKeyIds()
     17         {
     18             return _keyRings.Keys;
     19         }
     20 
     21         public string CurrentKeyId => NewestActivationKey(DateTimeOffset.Now)?.Element("key")?.Attribute("id")?.Value ?? GenerateKey(_dirInfo)?.Element("key")?.Attribute("id")?.Value;
     22 
     23         public string this[string keyId] =>
     24             GetAllKeyIds().FirstOrDefault(id => id == keyId) ?? throw new KeyNotFoundException();
     25 
     26         private void ReadKeys(DirectoryInfo dirInfo)
     27         {
     28             foreach (var fileInfo in dirInfo.GetFiles().Where(f => f.Extension == ".xml"))
     29             {
     30                 using (var stream = fileInfo.OpenRead())
     31                 {
     32                     XDocument xmlDoc = XDocument.Load(stream);
     33 
     34                     _keyRings.TryAdd(xmlDoc.Element("key")?.Attribute("id")?.Value, xmlDoc);
     35                 }
     36             }
     37         }
     38 
     39         private XDocument GenerateKey(DirectoryInfo dirInfo)
     40         {
     41             var now = DateTimeOffset.Now;
     42             if (!_keyRings.Any(item =>
     43                 DateTimeOffset.Parse(item.Value.Element("key")?.Element("activationDate")?.Value) <= now
     44                 && DateTimeOffset.Parse(item.Value.Element("key")?.Element("expirationDate")?.Value) > now))
     45             {
     46                 lock (_locker)
     47                 {
     48                     if (!_keyRings.Any(item =>
     49                         DateTimeOffset.Parse(item.Value.Element("key")?.Element("activationDate")?.Value) <= now
     50                         && DateTimeOffset.Parse(item.Value.Element("key")?.Element("expirationDate")?.Value) > now))
     51                     {
     52                         var masterKeyId = Guid.NewGuid().ToString();
     53 
     54                         XDocument xmlDoc = new XDocument();
     55                         xmlDoc.Declaration = new XDeclaration("1.0", "utf-8", "yes");
     56 
     57                         XElement key = new XElement("key");
     58                         key.SetAttributeValue("id", masterKeyId);
     59                         key.SetAttributeValue("version", 1);
     60 
     61                         XElement creationDate = new XElement("creationDate");
     62                         creationDate.SetValue(now);
     63 
     64                         XElement activationDate = new XElement("activationDate");
     65                         activationDate.SetValue(now);
     66 
     67                         XElement expirationDate = new XElement("expirationDate");
     68                         expirationDate.SetValue(now.AddDays(90));
     69 
     70                         XElement encryption = new XElement("encryption");
     71                         encryption.SetAttributeValue("BlockSize", 128);
     72                         encryption.SetAttributeValue("KeySize", 256);
     73                         encryption.SetAttributeValue("FeedbackSize", 128);
     74                         encryption.SetAttributeValue("Padding", PaddingMode.PKCS7);
     75                         encryption.SetAttributeValue("Mode", CipherMode.CBC);
     76 
     77                         SecurityUtil.AesProtector protector = new SecurityUtil.AesProtector();
     78                         XElement masterKey = new XElement("masterKey");
     79                         masterKey.SetValue(protector.GenerateKey().ToBase64String());
     80 
     81                         XElement iv = new XElement("iv");
     82                         iv.SetValue(protector.GenerateIV().ToBase64String());
     83 
     84                         xmlDoc.Add(key);
     85                         key.Add(creationDate);
     86                         key.Add(activationDate);
     87                         key.Add(expirationDate);
     88                         key.Add(encryption);
     89                         encryption.Add(masterKey);
     90                         encryption.Add(iv);
     91 
     92                         xmlDoc.Save(
     93                             $@"{dirInfo.FullName}key-{masterKeyId}.xml");
     94 
     95                         _keyRings.Add(masterKeyId, xmlDoc);
     96 
     97                         return xmlDoc;
     98                     }
     99 
    100                     return NewestActivationKey(now);
    101                 }
    102             }
    103 
    104             return NewestActivationKey(now);
    105         }
    106 
    107         private XDocument NewestActivationKey(DateTimeOffset now)
    108         {
    109             return _keyRings.Where(item =>
    110                     DateTimeOffset.Parse(item.Value.Element("key")?.Element("activationDate")?.Value) <= now
    111                     && DateTimeOffset.Parse(item.Value.Element("key")?.Element("expirationDate")?.Value) > now)
    112                 .OrderByDescending(item =>
    113                     DateTimeOffset.Parse(item.Value.Element("key")?.Element("expirationDate")?.Value)).FirstOrDefault().Value;
    114         }
    115     }

           这两个类也是注册到 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 会用所有钥匙加密一遍然后查找是否有精确匹配的记录。钥匙的有效期越短,随着网站运行时间的增加,钥匙数量会增加,要尝试的钥匙也会跟着增加,最后对系统性能产生影响。当然这可以用缓存来缓解。

    效果预览:

           转载请完整保留以下内容,未经授权删除以下内容进行转载盗用的,保留追究法律责任的权利!

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

      完整源代码:Github

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

  • 相关阅读:
    本人实操赚钱项目:月入10万的冷门玩法,人人可操作!
    赚钱项目:1万粉丝的公众号,年赚15万!
    AI 时代下的海量业务智能监控实践
    python 转xe7xbdx97xe5x87xbd 为中文
    python xb5xe7xc6xb1xc7xb0xd6xc3xd6xf7xbbxfa
    12.2 新特性:RMAN 自动恢复到 REDO 终点的步骤简化
    深入认识CSS的块级元素
    深入认识CSS的块级元素
    深入认识CSS的块级元素
    SAP WM TO Print Control设置里,Movement Type 的优先级更高
  • 原文地址:https://www.cnblogs.com/coredx/p/12210232.html
Copyright © 2020-2023  润新知