前言

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. git 安装及基本配置

    git 基本上来说是开发者必备工具了,在服务器里没有 git 实在不太能说得过去.何况,没有 git 的话,面向github编程 从何说起,如同一个程序员断了左膀右臂. 你对流程熟悉后,只需要一分钟便 ...

  2. tf.concat()

    转载自:https://blog.csdn.net/appleml/article/details/71023039 https://www.cnblogs.com/mdumpling/p/80534 ...

  3. js求1到任意数之间的所有质数

    何为质数: 只能被1 和 自身 整除的数; 方法: 利用js中求模, 看是否有余数. ---> 3%2 = 1; 5%2 = 3......... 代码如下: function test (n) ...

  4. 【codeforces 764A】Taymyr is calling you

    time limit per test1 second memory limit per test256 megabytes inputstandard input outputstandard ou ...

  5. 解决IDEA使用terminal时 git log 乱码

    1.配置环境变量:  变量名称-LESSCHARSET 变量值:utf-8 2.重启IDEA

  6. 有限状态机FSM和层次状态机HSM

    前言 就单片机而言,程序可以分为两类:带操作系统的程序和前后台程序:前后台程序从架构上又分为顺序机和状态机. 广义地说, 任何一个程序都是一个状态机, 因为它总是要记住一些状态, 然后根据输入进行输出 ...

  7. django框架(1)

    一什么是web框架? 框架,即framework,特指为解决一个开放性问题而设计的具有一定约束性的支撑结构,使用框架可以帮你快速开发特定的系统,简单地说,就是你用别人搭建好的舞台来做表演. 对于所有的 ...

  8. DEVOPS技术实践_18:Jenkins的Pinpeline对于参数的使用

    因为最近使用Pipeline声明式语法构建项目,但是最近项目的参数设置较多,特地的来学习一下关于参数的调用和测试,主要式从一个大神那里学习的,结尾回贴上大神的博客链接 1 构建一个pipeline项目 ...

  9. Java锁对象和条件对象的使用

    锁对象 临界区:临界区是一个特殊的代码段,该代码段访问某种特殊的公共资源,该资源同一时间只允许一个线程使用. Java中可以使用锁对象创造一个临界区: myLock.lock(); try { 关键代 ...

  10. 使用ASP.NET Core 3.x 构建 RESTful API - 4.2 过滤和搜索

    向Web API传递参数 数据可以通过多种方式来传给API. Binding Source Attributes 会告诉 Model 的绑定引擎从哪里找到绑定源. 共有以下六种 Binding Sou ...