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 的所有,其中一个就是隐私数据保护。html
乍一看,隐私数据保护是个什么东西,感受好像知道,但又说不清楚。确实这个东西光说很难解释清楚,那就直接上图:git
这是用户表的一部分,有没有发现问题所在?用户名和 Email 字段变成了一堆看不懂的东西。仔细看会发现这串乱码好像还有点规律:guid + 冒号 + 貌似是 base64 编码的字符串,固然这串字符串去在线解码结果仍是一堆乱码,好比 id 为 1 的 UserName :svBqhhluYZSiPZVUF4baOQ== 在线解码后是 ²ðjna¢=TÚ9 。github
这就是隐私数据保护,若是没有这个功能,那么用户名是明文存储的,虽然密码依然是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 的实现。接口包含一、获取当前正在使用的钥匙编号的只读属性,用于提供加密钥匙;二、根据钥匙编号获取字符串的索引器(我这里就是原样返回的。。。);三、获取全部钥匙编号的方法。ui
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_Data\AesDataProtectionKey"); 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_Data\AesDataProtectionKey"); 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 。
有几个要特别注意的点:
一、在有数据的状况下不要随便开启或关闭数据保护功能,不然可能致使严重后果。
二、钥匙必定要保护好,保存好。不然可能泄露用户数据或者再也没法解密用户数据,从删库到跑路那种 Shift + Del 的事千万别干。
三、被保护的字段没法在数据库端执行模糊搜索,只能精确匹配。若是但愿进行数据分析,只能先用 Identity 把数据读取到内存才能继续作其余事。
四、钥匙的有效期不宜太短,由于在用户登陆时 Identity 并不知道用户是何时注册的,应该用哪一个钥匙,因此 Identity 会用全部钥匙加密一遍而后查找是否有精确匹配的记录。钥匙的有效期越短,随着网站运行时间的增长,钥匙数量会增长,要尝试的钥匙也会跟着增长,最后对系统性能产生影响。固然这能够用缓存来缓解。
效果预览:
转载请完整保留如下内容,未经受权删除如下内容进行转载盗用的,保留追究法律责任的权利!
本文地址:http://www.javashuo.com/article/p-zstjfggj-es.html
完整源代码:Github
里面有各类小东西,这只是其中之一,不嫌弃的话能够Star一下。