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

来源:脚本之家  责任编辑:小易  

为什么都说好奇心害死猫,而不是好奇心害死狗害死猪呢?原因很简单,就是因为这个傲娇又高冷的主子有事没事的就爱作死!1、真的不知道,TM怎么上去的,这是要上天和太阳肩并肩的节奏啊。2、我说喵被卡住是有原因的,真的不是盒子太小,而是你太肥了。3、买卫生纸还送只猫?哪里买的还有吗,给我来一打~4、调料已经买好,就等着开火烤了,就不知道猫肉味道咋样,从来没吃过啊。5、好好好,你腰细你说话,但你特么倒是出来啊!真的是不作就不会死啊~6、快递到了,麻烦签收一下。7、喵:铲屎的你瞅啥?还不快来救驾!!!喵星人永远都是这么可爱,人前高冷人后逗逼,难怪这么多铲屎的为他着迷。

前言

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 。

二手车里程表的问题比较复杂了,今天给大家说说普及下。中国与欧美日本不同,国外汽车流通发达国家汽车从生产到报废基本所有环节都会有正规记录并且可以商业查询,在中国出了4S店以后的维修就很难有正规渠道查询了,保险记录也都不能方面的开放,公安系统也是如此,信息壁垒特别明显。里程表调表在美国是违法犯罪,判刑6-12个月,罚金也比较高,日本二手车从业者调表就会被行业通告无法再从事这个行业,并且记录在案,不诚信的会影响个人生活工作。在中国二手车调整里程表在某些情况下较多,不仅是中介行为,一些客户为了多卖点钱也会在出售前调表,整个社会诚信体系和信息公开体系是很大问题,此外也需要国家相关立法方面健全,目前涉及调

这就是隐私数据保护,如果没有这个功能,那么用户名是明文存储的,虽然密码依然是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 会用所有钥匙加密一遍然后查找是否有精确匹配的记录。钥匙的有效期越短,随着网站运行时间的增加,钥匙数量会增加,要尝试的钥匙也会跟着增加,最后对系统性能产生影响。当然这可以用缓存来缓解。

效果预览:

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

  完整源代码:Github

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

扩展阅读,根据您访问的内容系统为您准备了以下内容,希望对您有帮助。

aspnetcore2使用IdentityServer4完成分布式部署认证失效

我想问的是.NET Core 2.0好用吗?我也想转手做一下,又怕又是跟之前的什么mono一个模子刻出来的。现在已经有很多人在尝试了么?

  • 本文相关:
  • .net core3 用windows 桌面应用开发asp.net core网站
  • asp.net core 3.0 grpc拦截器的使用
  • asp.net core 3.0使用grpc的具体方法
  • 浅谈asp.net core的几种托管方式
  • asp.net core 授权详解
  • vs.net控件updatepanel实现无刷新的方法
  • 一个简单mvc5 + ef6示例分享
  • asp.net中mvc使用ajax调用jsonresult方法并返回自定义错误信息
  • aspnetpager分页控件定义及应用样式示例介绍
  • c# quoted-printable编码、解码
  • asp.net实现form认证的一些使用技巧(必看篇)
  • asp.net 在下载文件时对其重命名的思路及实现方法
  • 批量账号的login测试功能实现
  • visual studio 2017 15.5 正式发布!性能再提升
  • asp.net 利用npoi导出excel通用类的方法
  • aspnetcore2使用IdentityServer4完成分布式部署认...
  • 免责声明 - 关于我们 - 联系我们 - 广告联系 - 友情链接 - 帮助中心 - 频道导航
    Copyright © 2017 www.zgxue.com All Rights Reserved