前言

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 又是干嘛的?直接把加密的内容存进去不就完了。这其实是微软开发框架注重细节的最佳体现,接下来结合代码就能一探究竟。

启用隐私数据保护

 //注册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", ); 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()); XElement encryption = new XElement("encryption");
encryption.SetAttributeValue("BlockSize", );
encryption.SetAttributeValue("KeySize", );
encryption.SetAttributeValue("FeedbackSize", );
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一下。

Asp.Net Core Identity 隐私数据保护的更多相关文章

  1. IdentityServer(12)- 使用 ASP.NET Core Identity

    IdentityServer具有非常好的扩展性,其中用户及其数据(包括密码)部分你可以使用任何想要的数据库进行持久化. 如果需要一个新的用户数据库,那么ASP.NET Core Identity是你的 ...

  2. ASP.NET Core Identity Hands On(1)——Identity 初次体验

    ASP.NET Core Identity是用于构建ASP.NET Core Web应用程序的成员资格系统,包括成员资格.登录和用户数据存储 这是来自于 ASP.NET Core Identity 仓 ...

  3. ASP.NET Core Identity Hands On(2)——注册、登录、Claim

    上一篇文章(ASP.NET Core Identity Hands On(1)--Identity 初次体验)中,我们初识了Identity,并且详细分析了AspNetUsers用户存储表,这篇我们将 ...

  4. IdentityServer4 中文文档 -14- (快速入门)使用 ASP.NET Core Identity

    IdentityServer4 中文文档 -14- (快速入门)使用 ASP.NET Core Identity 原文:http://docs.identityserver.io/en/release ...

  5. IdentityServer4【QuickStart】之使用asp.net core Identity

    使用asp.net core Identity IdentityServer灵活的设计中有一部分是可以将你的用户和他们的数据保存到数据库中的.如果你以一个新的用户数据库开始,那么,asp.net co ...

  6. ASP.NET Core Identity 实战(2)——注册、登录、Claim

    上一篇文章(ASP.NET Core Identity Hands On(1)--Identity 初次体验)中,我们初识了Identity,并且详细分析了AspNetUsers用户存储表,这篇我们将 ...

  7. ASP.NET Core Identity 实战(4)授权过程

    这篇文章我们将一起来学习 Asp.Net Core 中的(注:这样描述不准确,稍后你会明白)授权过程 前情提要 在之前的文章里,我们有提到认证和授权是两个分开的过程,而且认证过程不属于Identity ...

  8. 用一个应用场景理解ASP.NET Core Identity是什么?

    目录 前言 基于声明的认证(Claims-based Authentication) 应用场景一 在ASP.NET Core 中Identity是如何实现的 类ClaimsPrincipal 考察另外 ...

  9. 用例子看ASP.NET Core Identity是什么?

    原文:用例子看ASP.NET Core Identity是什么? 目录 前言 基于声明的认证(Claims-based Authentication) Claim 在ASP.NET Core Iden ...

随机推荐

  1. 清除SVN未版控文件

    用Git时,git clean -df 可以清除所有没有add的文件,得到一个干净的工作空间. 用SVN没有这样的命令,当然可以 svn export 得到一个干净的工作空间,但会花很长时间,而且没有 ...

  2. win10系统激活 快捷方式

    系统不定期就会提示激活,每次激活都是找各种工具折腾,今天捣鼓简单的脚本直接激活~~ 首先查看自己系统的版本,后面才能找到合适的激活码 win+R 启动程序 输入 winver 即可查看系统版本 2.查 ...

  3. 2018-8-10-win10-uwp-win2d-使用-Path-绘制界面

    title author date CreateTime categories win10 uwp win2d 使用 Path 绘制界面 lindexi 2018-08-10 19:17:19 +08 ...

  4. P1036 最大公约数

    题目描述 给你两个正整数A和B,求它们的最大公约数. 输入格式 两个正整数 \(A,B(1 \le A,B \le 10^9)\) . 输出格式 一个整数,表示A和B的最大公约数. 样例输入 6 8 ...

  5. 用es5实现模板字符串

    废话不多说,主要是利用正则表达式replace+eval动态取值(纯属娱乐) String.prototype.myReplace = function(){ return this.replace( ...

  6. CentOS 7 修改root密码

    1.开机,在启动菜单上选择CentOS Linux (3.10**.**.x86**) 7 (Core) 按下e,进入编辑模式2.将光标一直移动到 LANG=en_US.UTF-8 后面(如果找不到, ...

  7. k8s数据持久化实验

    Step 1:创建PV ============================================ apiVersion: v1kind: PersistentVolumemetadat ...

  8. Android5_浅谈Java的package机制

    当代码量越来越大,类越来越多.尤其会增加同名类的风险.所以对类进行管理就显得非常重要. 包(package)机制是java中管理类的重要手段. 包名的命名方式:业内默认的做法是使用公司的网络域名的倒写 ...

  9. Android Library的依赖方式及发布(转)

    还是那句老话,好记性不然烂笔头,在此整理 Android Studio 依赖相关 以及 如何发布项目到 JCenter Android Studio 添加依赖Module 依赖module 依赖是指在 ...

  10. win10 配置g++环境

    一.配置g++编译器的环境 1.将g++编译器的位置添加到环境变量path中,安装了C++ IDE的可以在IDE的安装目录下寻找 例如:C:\MySoftware\dev\Dev-Cpp\MinGW6 ...