http://www.ciker.net/ciker/ciker.asp?ciker=338
摘要:
1 加Salt散列
2 ASP.NET 2.0 Membership中与密码散列有关的代码
声明:本文所罗列之源代码均通过Reflector取自.NET Framework类库,引用这些代码仅出于学习和研究的目的。
其实,对密码进行散列存储不是一个新鲜话题了,解决起来也不是很难,但很多人还是不大了解。这个小文只是强调一下“加Salt散列”这个简单的技术,并给出ASP.NET Membership所使用的代码。
本来打算写一篇介绍如何实现用户登录功能的文章的,但因为时间有限,所以先介绍一下密码的散列,下一篇再介绍用户登录。
----
1 密码必须散列存储
(内容略)
2 加Salt散列
我们知道,如果直接对密码进行散列,那么黑客(统称那些有能力窃取用户数据并企图得到用户密码的人)可以对一个已知密码进行散列,然后通过对比散列值得到某用户的密码。换句话说,虽然黑客不能取得某特定用户的密码,但他可以知道使用特定密码的用户有哪些。
加Salt可以一定程度上解决这一问题。所谓加Salt,就是加点“佐料”。其基本想法是这样的——当用户首次提供密码时(通常是注册时),由系统自动往这个密码里撒一些“佐料”,然后再散列。而当用户登录时,系统为用户提供的代码撒上同样的“佐料”,然后散列,再比较散列值,已确定密码是否正确。
这里的“佐料”被称作“Salt值”,这个值是由系统随机生成的,并且只有系统知道。这样,即便两个用户使用了同一个密码,由于系统为它们生成的salt值不同,他们的散列值也是不同的。即便黑客可以通过自己的密码和自己生成的散列值来找具有特定密码的用户,但这个几率太小了(密码和salt值都得和黑客使用的一样才行)。
下面详细介绍一下加Salt散列的过程。介绍之前先强调一点,前面说过,验证密码时要使用和最初散列密码时使用“相同的”佐料。所以Salt值是要存放在数据库里的。

图1. 用户注册
如图1所示,注册时,
1)用户提供密码(以及其他用户信息);
2)系统为用户生成Salt值;
3)系统将Salt值和用户密码连接到一起;
4)对连接后的值进行散列,得到Hash值;
5)将Hash值和Salt值分别放到数据库中。

图2. 用户登录
如图2所示,登录时,
1)用户提供用户名和密码;
2)系统通过用户名找到与之对应的Hash值和Salt值;
3)系统将Salt值和用户提供的密码连接到一起;
4)对连接后的值进行散列,得到Hash'(注意有个“撇”);
5)比较Hash和Hash'是否相等,相等则表示密码正确,否则表示密码错误。
3 ASP.NET 2.0 Membership中的相关代码
(省略关于Membership的介绍若干字)
本文Anders Liu仅研究了SqlMembershipProvider,该类位于System.Web.dll,System.Web.Security命名空间中。
首先,要使用Membership,必须先用aspnet_regsql.exe命令来配置数据库,该工具会向现有数据库中添加一系列表和存储过程等,配置好的数据库中有一个表aspnet_Membership,就是用于存放用户帐户信息的。其中我们所关注的列有三个——Password、PasswordFormat和PasswordSalt。
Password存放的是密码的散列值,PasswordFormat存放用于散列密码所使用的算法,PasswordSalt就是系统生成的Salt值了。
注册时用到了该类的CreateUser方法,该方法主要代码如下:
public override MembershipUser CreateUser(string username, string password, string email, string passwordQuestion, string passwordAnswer, bool isApproved, object providerUserKey, out MembershipCreateStatus status) { string str3; MembershipUser user; if (!SecUtility.ValidateParameter(ref password, true, true, false, 0x80)) { status = MembershipCreateStatus.InvalidPassword; return null; } // 生成salt值 string salt = base.GenerateSalt(); // 结合salt值对密码进行散列 string objValue = base.EncodePassword(password, (int) this._PasswordFormat, salt); if (objValue.Length > 0x80) { status = MembershipCreateStatus.InvalidPassword; return null; } if (passwordAnswer != null) { passwordAnswer = passwordAnswer.Trim(); } if (!string.IsNullOrEmpty(passwordAnswer)) { if (passwordAnswer.Length > 0x80) { status = MembershipCreateStatus.InvalidAnswer; return null; } str3 = base.EncodePassword(passwordAnswer.ToLower(CultureInfo.InvariantCulture), (int) this._PasswordFormat, salt); } else { str3 = passwordAnswer; } if (!SecUtility.ValidateParameter(ref str3, this.RequiresQuestionAndAnswer, true, false, 0x80)) { status = MembershipCreateStatus.InvalidAnswer; return null; } if (!SecUtility.ValidateParameter(ref username, true, true, true, 0x100)) { status = MembershipCreateStatus.InvalidUserName; return null; } if (!SecUtility.ValidateParameter(ref email, this.RequiresUniqueEmail, this.RequiresUniqueEmail, false, 0x100)) { status = MembershipCreateStatus.InvalidEmail; return null; } if (!SecUtility.ValidateParameter(ref passwordQuestion, this.RequiresQuestionAndAnswer, true, false, 0x100)) { status = MembershipCreateStatus.InvalidQuestion; return null; } if ((providerUserKey != null) && !(providerUserKey is Guid)) { status = MembershipCreateStatus.InvalidProviderUserKey; return null; } if (password.Length < this.MinRequiredPasswordLength) { status = MembershipCreateStatus.InvalidPassword; return null; } int num = 0; for (int i = 0; i < password.Length; i++) { if (!char.IsLetterOrDigit(password, i)) { num++; } } if (num < this.MinRequiredNonAlphanumericCharacters) { status = MembershipCreateStatus.InvalidPassword; return null; } if ((this.PasswordStrengthRegularExpression.Length > 0) && !Regex.IsMatch(password, this.PasswordStrengthRegularExpression)) { status = MembershipCreateStatus.InvalidPassword; return null; } ValidatePasswordEventArgs e = new ValidatePasswordEventArgs(username, password, true); this.OnValidatingPassword(e); if (e.Cancel) { status = MembershipCreateStatus.InvalidPassword; return null; } try { SqlConnectionHolder connection = null; try { connection = SqlConnectionHelper.GetConnection(this._sqlConnectionString, true); this.CheckSchemaVersion(connection.Connection); DateTime time = this.RoundToSeconds(DateTime.UtcNow); SqlCommand command = new SqlCommand("dbo.aspnet_Membership_CreateUser", connection.Connection); command.CommandTimeout = this.CommandTimeout; command.CommandType = CommandType.StoredProcedure; command.Parameters.Add(this.CreateInputParam("@ApplicationName", SqlDbType.NVarChar, this.ApplicationName)); command.Parameters.Add(this.CreateInputParam("@UserName", SqlDbType.NVarChar, username)); command.Parameters.Add(this.CreateInputParam("@Password", SqlDbType.NVarChar, objValue)); command.Parameters.Add(this.CreateInputParam("@PasswordSalt", SqlDbType.NVarChar, salt)); command.Parameters.Add(this.CreateInputParam("@Email", SqlDbType.NVarChar, email)); command.Parameters.Add(this.CreateInputParam("@PasswordQuestion", SqlDbType.NVarChar, passwordQuestion)); command.Parameters.Add(this.CreateInputParam("@PasswordAnswer", SqlDbType.NVarChar, str3)); command.Parameters.Add(this.CreateInputParam("@IsApproved", SqlDbType.Bit, isApproved)); command.Parameters.Add(this.CreateInputParam("@UniqueEmail", SqlDbType.Int, this.RequiresUniqueEmail ? 1 : 0)); command.Parameters.Add(this.CreateInputParam("@PasswordFormat", SqlDbType.Int, (int) this.PasswordFormat)); command.Parameters.Add(this.CreateInputParam("@CurrentTimeUtc", SqlDbType.DateTime, time)); SqlParameter parameter = this.CreateInputParam("@UserId", SqlDbType.UniqueIdentifier, providerUserKey); parameter.Direction = ParameterDirection.InputOutput; command.Parameters.Add(parameter); parameter = new SqlParameter("@ReturnValue", SqlDbType.Int); parameter.Direction = ParameterDirection.ReturnValue; command.Parameters.Add(parameter); command.ExecuteNonQuery(); int num3 = (parameter.Value != null) ? ((int) parameter.Value) : -1; if ((num3 < 0) || (num3 > 11)) { num3 = 11; } status = (MembershipCreateStatus) num3; if (num3 != 0) { return null; } providerUserKey = new Guid(command.Parameters["@UserId"].Value.ToString()); time = time.ToLocalTime(); user = new MembershipUser(this.Name, username, providerUserKey, email, passwordQuestion, null, isApproved, false, time, time, time, time, new DateTime(0x6da, 1, 1)); } finally { if (connection != null) { connection.Close(); connection = null; } } } catch { throw; } return user; } |
|
其中我们可以看到两个比较令人感兴趣的方法:GenerateSalt和EncodePassword。由于本文讨论的仅仅是密码的散列,而不是整个用户注册过程,所以这里只对这两个函数进行分析。
这两个方法来自于SqlMembershipProvider的父类,MembershipProvider。
GenerateSalt方法的代码比较简单:
internal string GenerateSalt() { byte[] data = new byte[0x10]; new RNGCryptoServiceProvider().GetBytes(data); return Convert.ToBase64String(data); } |
|
但是要注意的是,在这种方法里Salt值的高度随机性是安全的保障,所以不能简单的使用Random来获取随机数,而应该使用更安全的方式。这里使用了RNGCryptoServiceProvider来生成随机数。
EncodePassword方法的代码也不难:
internal string EncodePassword(string pass, int passwordFormat, string salt) { if (passwordFormat == 0) { return pass; } // 将密码和salt值转换成字节形式并连接起来 byte[] bytes = Encoding.Unicode.GetBytes(pass); byte[] src = Convert.FromBase64String(salt); byte[] dst = new byte[src.Length + bytes.Length]; byte[] inArray = null; Buffer.BlockCopy(src, 0, dst, 0, src.Length); Buffer.BlockCopy(bytes, 0, dst, src.Length, bytes.Length); // 选择算法,对连接后的值进行散列 if (passwordFormat == 1) { HashAlgorithm algorithm = HashAlgorithm.Create(Membership.HashAlgorithmType); if ((algorithm == null) && Membership.IsHashAlgorithmFromMembershipConfig) { RuntimeConfig.GetAppConfig().Membership.ThrowHashAlgorithmException(); } inArray = algorithm.ComputeHash(dst); } else { inArray = this.EncryptPassword(dst); } // 以字符串形式返回散列值 return Convert.ToBase64String(inArray); } |
|
这段代码的作用就是,首先将密码和salt值转换成字节数组(分别放到bytes和src数组中),然后拼接到一起(dst数组)。之后再根据Web.config中设置的加密算法,对这个拼接值进行散列,最后把散列值转换成字符串形式返回。
最后,用户登录时,将会使用SqlMembershipProvider的CheckPassword方法对密码进行检验。该方法有两种重载形式,最为完整的一种如下所示:
private bool CheckPassword(string username, string password, bool updateLastLoginActivityDate, bool failIfNotApproved, out string salt, out int passwordFormat) { SqlConnectionHolder connection = null; string str; // 密码散列值 int num; int num2; int num3; bool flag2; DateTime time; DateTime time2; // 从数据库中拿到Hash和Salt this.GetPasswordWithFormat(username, updateLastLoginActivityDate, out num, out str, out passwordFormat, out salt, out num2, out num3, out flag2, out time, out time2); if (num != 0) { return false; } if (!flag2 && failIfNotApproved) { return false; } // 对用户刚刚输入的密码进行散列 string str2 = base.EncodePassword(password, passwordFormat, salt);
// 比较两个散列值,看密码是否相等 bool objValue = str.Equals(str2); if ((objValue && (num2 == 0)) && (num3 == 0)) { return true; } try { try { connection = SqlConnectionHelper.GetConnection(this._sqlConnectionString, true); this.CheckSchemaVersion(connection.Connection); SqlCommand command = new SqlCommand("dbo.aspnet_Membership_UpdateUserInfo", connection.Connection); DateTime utcNow = DateTime.UtcNow; command.CommandTimeout = this.CommandTimeout; command.CommandType = CommandType.StoredProcedure; command.Parameters.Add(this.CreateInputParam("@ApplicationName", SqlDbType.NVarChar, this.ApplicationName)); command.Parameters.Add(this.CreateInputParam("@UserName", SqlDbType.NVarChar, username)); command.Parameters.Add(this.CreateInputParam("@IsPasswordCorrect", SqlDbType.Bit, objValue)); command.Parameters.Add(this.CreateInputParam("@UpdateLastLoginActivityDate", SqlDbType.Bit, updateLastLoginActivityDate)); command.Parameters.Add(this.CreateInputParam("@MaxInvalidPasswordAttempts", SqlDbType.Int, this.MaxInvalidPasswordAttempts)); command.Parameters.Add(this.CreateInputParam("@PasswordAttemptWindow", SqlDbType.Int, this.PasswordAttemptWindow)); command.Parameters.Add(this.CreateInputParam("@CurrentTimeUtc", SqlDbType.DateTime, utcNow)); command.Parameters.Add(this.CreateInputParam("@LastLoginDate", SqlDbType.DateTime, objValue ? utcNow : time)); command.Parameters.Add(this.CreateInputParam("@LastActivityDate", SqlDbType.DateTime, objValue ? utcNow : time2)); SqlParameter parameter = new SqlParameter("@ReturnValue", SqlDbType.Int); parameter.Direction = ParameterDirection.ReturnValue; command.Parameters.Add(parameter); command.ExecuteNonQuery(); num = (parameter.Value != null) ? ((int) parameter.Value) : -1; return objValue; } finally { if (connection != null) { connection.Close(); connection = null; } } } catch { throw; } return objValue; } |
|
这个代码首先通过GetPasswordWithFormat得到了Hash值(变量str)和Salt值(变量salt),然后对用户输入的密码(参数password)进行与注册时一样的散列(只是salt值使用了数据库中现存的值)得到散列值str2,之后通过对比str和str2,就知道密码正确与否了。
4 小结
本文只是简单地介绍了加Salt散列的工作方式(而非原理)、ASP.NET 在Membership中对其的实现。通过本文大家虽然无法对加Salt加密的有点和原理“知其所以然”,但相信大家应该大致了解了这种方式的使用方法,并能通过修改Membership的代码实现自己的密码散列存储了。
由于时间有限,Anders Liu这篇文章写得很潦草,罗列了不少代码却没有系统性介绍,还望大家原谅。下一篇文章我将相对完整地介绍如何实现自己的用户登录(无需使用MembershipProvider,但同时也丧失了Login等控件为我们带来的便利)。 <!-- Search Google -->
分享到:
相关推荐
在本压缩包"salt_jisuan.zip"中,包含了一个名为"salt_jisuan"的小程序,这个程序专门用于计算海水的盐度场。对于初学者而言,这是一个非常实用的学习工具,可以帮助理解海洋盐度变化的原理和计算方法。 盐度的测量...
本文提出了一种基于SMOS卫星数据的BP神经网络盐度反演模型,该模型能够提供一种不依赖于物理机制的新方法来预测海表面盐度。 在本文中,我们还讨论了机器学习在盐度反演模型中的应用。机器学习是一种常用的方法,...
高盐度海水是通过煮沸海水与沙滤水调配而成,而低盐度海水则是利用蒸馏水与沙滤水调配。盐度的测量使用了盐度计,保证了实验的准确性。实验温度控制在28±0.5℃,pH值约为8.0,溶氧量约为7.0。 4. 实验结果 研究...
总之,全球海洋Argo温盐度剖面散点数据集是理解和研究全球海洋系统及其与气候变化关系的宝贵资料,其背后蕴含的科学价值不容忽视。通过深入分析这些数据,我们可以深化对海洋环境的理解,为应对全球气候变化提供科学...
根据盐度水平与特定生长率,饲料系数和蛋白质效率比之间关系的二次多项式拟合,得出结论:25.36-25.9是最适合盐生毕赤酵母生长性能的盐度范围。 在不同的盐度水平下测量了主体组成(水分,粗脂肪,粗蛋白和灰分)。...
软件的可视化界面使得操作更加直观便捷,用户可以通过监控软件远程无线监控养殖水体的盐度参数。 在技术应用方面,该系统以硬件开发为基础,涉及到单片机的编程、无线通讯技术、电子电路设计、信号处理等多个技术...
首先,文件标题“Matlab对基于二进制-XML混合格式的SMOS盐度数据的提取方法与实现.pdf”中蕴含的知识点主要集中在以下几个方面: 1. Matlab软件应用:Matlab是一种高性能的数学计算和可视化软件,广泛应用于数据...
标准盐度 温度 密度 盐质量 34 20 1.0255 34.68 34 21 1.0252 34.98 34 22 1.025 35.28 34 23 1.0248 35.58 34 24 1.0246 35.88 34 25 1.0243 36.18 34 26 1.0241 36.48 34 27 1.0239 36.78 34 28 1.0236 37.08 34 ...
基于STM32的海水盐度检测系统 器件: ①海水检测使用浑浊度传感器(淘宝一搜就搜到),AD采集; ②下位机嵌入式使用屏幕为OLED显示屏,spi-7线通信方式(配套资料及取模软件,清晰函数接口); ③温度传感器使用DS18B20...
本文主要探讨了高精度实验室盐度计的测量结果不确定度的分析与评定,特别是针对加拿大Guildline生产的8400B型实验室盐度计。文章首先介绍了海水盐度作为重要海洋参数的重要性,以及电导率测量方法在盐度测量中的广泛...
7. 按ENTER键保存修改后的值,系统会显示新的盐度(Salt (new)=xx.x%),询问是否接受。 8. 按ENTER键接受新盐度后,返回technician mode。最后,将工作模式切换回Normal mode,仪器恢复正常工作。 用户界面的四个...
5. **可视化**:可能包含了使用MATLAB的图形用户界面(GUI)或绘图函数来展示盐度分布图,帮助用户更好地理解结果。 6. **海洋环流模型**:如果更复杂,该程序可能还会涉及到将盐度数据与海洋环流模型结合,以研究...
结果显示,在盐度变化的初期,黄条鰤的消化酶活性、SOD活力和T4浓度均发生了显著变化,反映了其适应盐度变化的生理调节机制。特别是,在高盐度条件下,消化酶活性的增强和SOD活力的提升表明黄条鰤通过增强消化与抗...
于2013年2月在Gorgan农业科学与自然资源大学的园艺实验室中进行了一项研究,以评估盐度和硝酸钾对五种盐度(0,-2,-4,-6的鸡冠发芽的影响) ,以及-8 bar)和25°C时的三个硝酸钾水平(0%,0.2%和0.4%),基于...
(i)全球盐度数据集 全球河流化学数据集(GLORICH) 包括世界各地河流位置的多个水质参数,由汉堡大学的研究人员收集。 (ii)区域盐度数据集: 欧洲的数据来自欧洲环境署的水质数据库;Waterbase包含整个欧洲的河流、...
海面盐度是海洋学中的关键参数之一,它与海洋的热力学特性、生物活动以及全球水循环紧密相关。然而,降雨会显著影响海面盐度,因为雨水稀释海水,使得测量变得复杂。传统的海洋盐度测量方法,如CTD( conductivity, ...
本文首先介绍了孤立岛的特点,通过改变体积比通过声波降低混合水(海水和雨水)盐度的实验数据。 接下来,示出了对被照射声波的混合水的水质进行的主要分析结果,并且主要基于WHO饮用水水质标准进行了考虑。 最后,...