前面文章介绍了如何使用Identity在ASP.NET MVC中实现用户的注册、登录以及身份验证。这些功能都是与用户信息安全相关的功能,数据安全的重要性永远放在第一位。那么对于注册和登录功能来说要把密码及用户其它信息通过表单的形式安全的提交到服务器上,那么最适合的方法就是使用HTTPS(如果有条件或者有安全需求,应该所有请求都基于HTTPS,本章不涉及HTTPS的介绍),而在注册时用户的密码应该加密后保存在数据库中,包括登录时对用户名的验证也是对密码明文加密后再进行匹配,对于身份验证来说,服务器生成的用户信息字符串是必须进行加密的,其目的是保护用户信息并且能够让当前的服务器(或集群)能够识别。
本章将从以下几点对Identity中涉及到的加解密进行介绍:
● 常用的加密方法
● .Net对常用加密方法的实现
● Identity用户密码的加解密
● Identity用户身份信息的处理过程
● MachineKey的加解密
● 自定义Identity身份信息的验证(基于MachineKey)
常用的加密方法
软件中常用的加密方法分为两类,一类是密文可解密回明文的,而另一类是密文不可解密的。
对于可解密的这一类主要是通过对称加密算法及非对称加密算法,如DES、AES、RSA等,它们最主要的特点是需要“密钥”来进行加解密工作,如果密钥泄露了,那么就会造成安全问题。
而不可解密的这一类主要是通过MD5、SHA1这些单向Hash算法来提取“信息指纹”,已达到“加密”的效果,但这种方法也存在缺陷就是只要算法相同,那么对同一个字符串加密后的结果就是相同的,当黑客拿到了用户数据库,虽然用户密码是被加密存储,但是黑客可以通过建立“彩虹表”的方式破解密码。所以又出现了一种通过加“盐”的算法,通过加入特殊的“盐”来保证相同HASH算法相同字符串的加密不一致性,但如果“盐”泄露了黑客仍然能够破解,所以又有了“随机盐”。
参考:http://mp.weixin.qq.com/s?__biz=MzIwMzg1ODcwMw==&mid=2247486407&idx=1&sn=51dfbce7d04ab6faeb0f5a27a5bdcbf8&source=41#wechat_redirect
.Net对常用加密方法的实现
.Net的System.Security.Cryptography命名空间下包含了用于加解密的类型,这些类型有些是基于托管代码的,有些是基于Windows API的。
.Net的加解密类型位于system.dll程序集中(注:非windows平台下可以通过nuget安装System.Security.Cryptography.Primitives.dll)的System.Security.Cryptography命名空间下。并且将加密算法分为了三类:
● 对称加密算法:AES、DES等
● 非对称加密算法:RSA、ECDH 等
● HASH算法:SHA1、MD5等
另外要注意的是.Net中使用面向数据流的方式实现了对称加密和HASH算法。这样的设计可以通过串联的方式将多个加密算法合并在一起对“数据流”进行操作(这个东西有点类似于owin中间件的方式,可以根据需求动态的对数据加密进行处理)。
微软官方文档对加密算法的使用推荐:
● 数据保护:Aes
● 数据完整性:HMACSHA256、HMACSHA512
● 数字签名:ECDsa、 RSA
● 密钥交换:ECDiffieHellman、 RSA
● 随机数生成:RNGCryptoServiceProvider
● 通过密码生成Key(使用随机盐的Hash算法):Rfc2898DeriveBytes
更多信息参考文档:https://docs.microsoft.com/en-us/dotnet/standard/security/cryptography-model
Identity用户密码的加解密
用户的密码一般来说是一个长度较短的包含各种字符的字符串,而对用户密码加密的目的是避免用户密码在数据库中明文存储,明文存储密码会导致系统开发或运营人员对用户信息安全的威胁以及黑客攻击数据泄露导致的用户信息安全。所以一般来说加密密码使用无法解密的Hash算法“加密”。
根据前面文章的分析得知,用户的创建和密码的匹配都是通过Identity中的UserManager类型完成的:
1. 注册调用的代码:
2. 登录调用的代码(注:SignInManager位于Microsoft.AspNet.Identity.Owin程序集中):
但通过查看源码可知,SignInManager实际上也是通过UserManager来匹配密码的:
3. 所以根据上面的分析,用户密码的加密是在UserManager中完成的,而UserManager定义中有一个IPasswordHasher的接口,该接口定义了密码的Hash加密以及Hash后的密码校验:
IPasswordHasher的默认实现是PasswordHasher类型:
从代码中可以看到PassswordHasher又是通过一个名称为Crypto类型的静态方法完成加密和验证的:
Hash计算:
Hash验证:
从Crypto的代码中可以得出以下几点结论:
1. Identity中默认的密码加密基于Rfc2898算法(通过随机盐以及设置迭代次数来计算hash值的算法)。
2. 算法中的“盐”长度为16位,迭代计算次数为1000次(注:每次实例化Rfc2898DeriveBytes类型时会根据盐的长度,创建一个随机的数组。Rfc2898DeriveBytes的GetBytes的算法不在此详解,有兴趣可参考文档和源码)。
3. 加密时Identity将盐和加密后的结果进行了拼接,前16位数据为盐后面的是密码加密结果。
4. 密码的“解密”实际上是通过已经加密的结果先获取其前16位数据拿到盐,然后再对传入的密码和这个盐进行一次Hash,然后比较两次的Hash结果是否相同(注:Hash算法无法解密)。
如果要对Identity中的用户密码加密算法进行变更或者扩展,仅需实现新的IPasswordHasher,然后在创建UserManager实例时将其替换即可。
注:事实上如果黑客拿到了上面数据理论上仍旧是可以破解密码的,但由于盐是随机的,所以导致大批量破解会更加麻烦,这样哪怕数据泄露了也有时间进行一些补救,所以Rfc2898是一种常用的密码加密方式。
Identity用户身份信息的处理过程
Identity的用户身份信息相对于密码来说要复杂很多,因为密码仅仅是一个字符串,对一个字符串的加解密很容易,但是Identity的用户身份信息实际上是一个AuthenticationTicket实例:
那么Identity是如何对这个用户身份信息实例进行处理的呢?
1. 首先我们知道的是Identity通过app.UseCookieAuthentication方法在管道中添加了一个类型为CookieAuthenticationMiddleware的中间件,而通过对源码分析可以看到,该中间件中实际上是通过创建一个名为CookieAuthenticationHandler的内部类型,通过这个类型完成了请求时Cookie的获取、验证,验证失败的跳转以及响应时Cookie的写入等功能。
其中Cookie的加解密代码如下:
解密:先获取Cookie值,然后通过TicketDataFormat的Unprotect方法返回一个AuthenticationTicket实例:
加密:将AuthenticationTicket实例通过TicketDataFormat的Protect方法转换为一个加密后的字符串。
2. Identity对用户身份信息的处理主要是通过TicketDataFormat完成,从上面代码中可以看到TicketDataFormat是来来自Options。这里的Options实际上就是app.UseCookieAuthentication方法中的参数CookieAuthenticationOptions:
TicketDataFormat默认值是在构造方法中创建的,它需要一个protector(注:Protector实际上就是加解密的组件,本章后面详解)
3. TicketDataFormat的职责:
由于TicketDataFormat是继承于SecureDataFormat类型,并且仅仅是在构造方法中硬编码了传入基类的参数,所以其功能实际上是基类实现的:
职责一:数据“保护”,先通过序列化器将泛型类型TData进行序列化(这里的TData实际上是AuthenticationTicket类型),然后通过加密组件对序列化后的二进制进行加密,最后通过编码器将二进制数据转换为Base64Url字符串,代码如下图:
这里要注意以下两点:
1). 序列化器是由TicketDataFormat构造方法中硬编码的,其真实类型为TicketSerializer(对于序列化这个概念,实际上就是将一个程序中的内存实例,用二进制数据或者XML、Json等方式保存下来,然后需要使用的时候在通过这些数据把它反序列化为之前的内存实例,这里的TicketSerializer是一个二进制序列化器):
2). 编码器的名称为Base64Url与Base64编码器的区别是,由于Base64字符串中可能会存在斜杠(/)等特殊符号,但是这些符号在url中是无法被正确识别的,所以Base64Url对这些字符进行了特殊处理:
职责二:数据的“解保护”实际上就是保护功能反过来:先将Base64Url字符串解码为二进制数据,然后对二进制数据解密,最后对解密后的数据进行反序列化:
而本章的重点实际上是在数据的加解密上,所以protector才是关注重点,这里的protector从上面的代码中可以看到是通过IAppBulider创建的:
前面的文章分析过,Owin的核心实际上是一个字典,所以通过Owin来获取的东西应该是保存在字典中的:
AppBuilder的初始化代码:
根据上面的分析得出,在没有指定特殊的数据保护器情况下,Identity使用MachineKeyDataProtector作为默认的数据保护器。
补充说明:
Identity中的身份验证的原理,实际上是获取到Cookie成功解密并反序列化为AuthenticationTicket实例后,将通过身份验证的Identity(该Identity中的IsAuthenticated属性为true)信息添加到HTTP请求的上下文中的。MVC中需要通过身份验证的访问控制就是通过请求上下文中Identity的IsAuthenticated属性完成判断的。
MachineKey的加解密
.Net中有一个名为MachineKey的组件,它用于Forms验证用户信息、asp.net 的View State以及跨进程的会话状态数据的加密和验证,MachineKey可以通过在web.config文件中加入以下的配置文件来对MachineKey的加解密、验证算法及其密钥进行配置,详情可参考文档:https://msdn.microsoft.com/en-us/library/w8h3skw9(v=vs.100).aspx
而上面分析知道Identity使用MachineKeyDataProtector作为数据保护器,而MachineKeyDataProtector实际上使用的就是MachineKey:
注:由于MachineKey相关代码比较复杂,本文中仅对其主要的一些对象以及加解密过程进行介绍:
MachineKey的主要相关对象:
● AspNetCryptoServiceProvider(内部类型):ASP.NET用其获取适合的加密组件。
● MachineKeySection:用于表示MachineKey的配置信息。
● MachineKeyCryptoAlgorithmFactory(内部类型):MachineKey的加密算法工厂,依赖MachineKeySection,可以从配置文件中获取加密算法类型。
● MachineKeyMasterKeyProvider(内部类型):密钥提供器,依赖MachineKeySection,可以从配置文件中获取密钥信息。
● MachineKeyDataProtectorFactory (内部类型):数据保护器工厂,用于创建自定义加解密类型(配置文件中可以通过alg:algorithm_name方法使用自定义的加密算法)。
● Purpose(内部类型):用于根据加密目的来生成真正用于加密和校验的密钥,Identity使用的目的为User_MacineKey_Protect,User_MacineKey_Protect的主目的为User.MachineKey.Protect,特殊目的为"Microsoft.Owin.Security.Cookies.CookieAuthenticationMiddleware", "ApplicationCookie","v1"(数据来自源码分析)。换句话说如果密钥相同,但是加密目的不一样,那么真实用于加解密的密钥也是不同的。
上图为Purpose的定义,从定义中也可以看出针对功能的不同如Forms验证的、角色信息的以及WebForm中一系列组件的目的均不相同。
● NetFXCryptoService(内部类型):MachineKey在.Net平台下使用的加解密服务组件。也是Identity中使用的身份信息加解密组件。
以下代码为NetFXCryptoService加解密的算法,其算法包括了数据加解密以及数据完整性校验两个部分:
加密:
1 public byte[] Protect(byte[] clearData) //claerData为需要加密的二进制数据 2 { 3 byte[] buffer4; 4 using (SymmetricAlgorithm algorithm = this._cryptoAlgorithmFactory.GetEncryptionAlgorithm()) //通过工厂获取加密算法,实际上就是使用默认的或配置文件指定的如AES等 5 { 6 algorithm.Key = this._encryptionKey.GetKeyMaterial();//Purpose通过配置文件获取加密密钥并根据实际目的派生出来的真实密钥 7 if (this._predictableIV) 8 { 9 algorithm.IV = CryptoUtil.CreatePredictableIV(clearData, algorithm.BlockSize); 10 } 11 else 12 { 13 algorithm.GenerateIV(); 14 } 15 byte[] iV = algorithm.IV; 16 using (MemoryStream stream = new MemoryStream()) 17 { 18 stream.Write(iV, 0, iV.Length); 19 using (ICryptoTransform transform = algorithm.CreateEncryptor()) 20 { 21 using (CryptoStream stream2 = new CryptoStream(stream, transform, CryptoStreamMode.Write)) 22 { 23 stream2.Write(clearData, 0, clearData.Length); 24 stream2.FlushFinalBlock(); 25 using (KeyedHashAlgorithm algorithm2 = this._cryptoAlgorithmFactory.GetValidationAlgorithm())//通过工厂获取数据校验的算法,该算法在配置文件中配置,如SHA1等 26 { 27 algorithm2.Key = this._validationKey.GetKeyMaterial();//Purpose通过配置文件获取的数据校验密钥并根据实际目的派生出来的真实密钥 28 byte[] buffer = algorithm2.ComputeHash(stream.GetBuffer(), 0, (int) stream.Length); 29 stream.Write(buffer, 0, buffer.Length); 30 buffer4 = stream.ToArray(); 31 } 32 } 33 } 34 } 35 } 36 return buffer4; 37 }
解密(加密的反过程):
1 public byte[] Unprotect(byte[] protectedData) 2 { 3 byte[] buffer3; 4 using (SymmetricAlgorithm algorithm = this._cryptoAlgorithmFactory.GetEncryptionAlgorithm()) 5 { 6 algorithm.Key = this._encryptionKey.GetKeyMaterial(); 7 using (KeyedHashAlgorithm algorithm2 = this._cryptoAlgorithmFactory.GetValidationAlgorithm()) 8 { 9 algorithm2.Key = this._validationKey.GetKeyMaterial(); 10 int offset = algorithm.BlockSize / 8; 11 int num2 = algorithm2.HashSize / 8; 12 int count = (protectedData.Length - offset) - num2; 13 if (count <= 0) 14 { 15 return null; 16 } 17 byte[] buffer = algorithm2.ComputeHash(protectedData, 0, offset + count); 18 if (!CryptoUtil.BuffersAreEqual(protectedData, offset + count, num2, buffer, 0, buffer.Length)) 19 { 20 buffer3 = null; 21 } 22 else 23 { 24 byte[] dst = new byte[offset]; 25 Buffer.BlockCopy(protectedData, 0, dst, 0, dst.Length); 26 algorithm.IV = dst; 27 using (MemoryStream stream = new MemoryStream()) 28 { 29 using (ICryptoTransform transform = algorithm.CreateDecryptor()) 30 { 31 using (CryptoStream stream2 = new CryptoStream(stream, transform, CryptoStreamMode.Write)) 32 { 33 stream2.Write(protectedData, offset, count); 34 stream2.FlushFinalBlock(); 35 buffer3 = stream.ToArray(); 36 } 37 } 38 } 39 } 40 } 41 } 42 return buffer3; 43 }
自定义Identity身份信息的验证(基于MachineKey)
本例将在Controller的Action方法中获取登录生成的Cookie值,并将其解密后反序列化成AuthenticactionTicket实例:
代码:
1 public ActionResult Index() 2 { 3 //1.从Cookie中获取加密后的用户信息字符串 4 var cookieStr = this.HttpContext.Request.Cookies[".AspNet.ApplicationCookie"].Value.ToString(); 5 //2.将用户信息字符串以Base64Url的方式转换为二进制数据 6 var cookieBytes = TextEncodings.Base64Url.Decode(cookieStr); 7 //3.转换后的二进制数据通过MachineKey进行解密(注:MachinKey默认使用User_MacineKey_Protect为主目的, 8 //特殊目的由Owin Cookie验证中间件提供) 9 var result = MachineKey.Unprotect(cookieBytes, 10 new string[] { "Microsoft.Owin.Security.Cookies.CookieAuthenticationMiddleware", 11 "ApplicationCookie", 12 "v1"}); 13 TicketSerializer ticketSerializer = new TicketSerializer(); 14 //4.将解密后的二进制数据反序列化为AuthenticationTicket实例 15 var ticket = ticketSerializer.Deserialize(result); 16 17 return View(); 18 }
登录后的运行结果:
注:MachineKey可以通过配置文件来改变加解密以及数据验证的算法及密钥,该配置文件可以通过IIS的“计算机密钥”功能来实现:
小结
本章在软件开发中常用的加密算法及其在.Net中的应用介绍的基础上,引出了Identity中用户密码以及用户信息的加解密的过程与方法,其中用户密码的加解密较为简单,而用户信息作为一个复杂的对象实例,在加解密之前还需要进行序列化与反序列的流程,另外也得知了对于用户信息的保护不仅仅是加密而且还附带了数据完整性验证功能。数据安全是一个非常重要的话题,而Identity的身份验证是默认ASP.NET MVC带有独立身份验证模板提供的功能,一个一分钟就能创建的应用程序模板就提供了如此复杂的用户数据安全保护功能,由此可见.Net的强大之处。
另外本章除了介绍Identity,实际上也是介绍了一种数据保护以及身份验证的方式,在没有使用Identity的情况下,仍旧可以使用其理念来打造一个符合自身需求的数据保护方案。
参考:
https://docs.microsoft.com/en-us/dotnet/standard/security/cryptography-model
https://msdn.microsoft.com/en-us/library/ff648652.aspx
https://www.rfc-editor.org/rfc/rfc2898.txt
https://www.codeproject.com/articles/16645/asp-net-machinekey-generator
http://www.cnblogs.com/happyhippy/archive/2006/12/23/601353.html
http://mp.weixin.qq.com/s?__biz=MzIwMzg1ODcwMw==&mid=2247486407&idx=1&sn=51dfbce7d04ab6faeb0f5a27a5bdcbf8&source=41#wechat_redirect