浅析ASP.NET 2.0的用户密码加密机制
点击次数:16 次 发布日期:2008-11-26 10:35:58 作者:源代码网
|
源代码网推荐 源代码网推荐 1 加Salt散列 源代码网推荐 2 ASP.NET 2.0 Membership中与密码散列有关的代码 源代码网推荐 源代码网推荐 声明:本文所罗列之源代码均通过Reflector取自.NET Framework类库,Anders Liu引用这些代码仅出于学习和研究的目的。 源代码网推荐 源代码网推荐 前一段关于密码的存储问题产生了一些讨论。我所看到的景象是,首先在cnbeta新闻中提到中国某银行将强制冻结密码过于简单(如6个8)的帐户,引发了争论。一方认为银行采用明文存放用户密码;另一方则认为,即便密码是经过散列存放的,但只要得到“6个8”的散列值,通过对比散列值也可以发现具有特定密码的用户。 源代码网推荐 源代码网推荐 后来在博客园(cnblogs.com)也看到有朋友发帖讨论了密码的散列存储,再后来在MVP的QQ群里也就这个问题小小议论了一番。 源代码网推荐 源代码网推荐 其实,对密码进行散列存储不是一个新鲜话题了,解决起来也不是很难,但很多人还是不大了解。Anders Liu这个小文只是强调一下“加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所示,登录时, 源代码网推荐 源代码网推荐 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方法,该方法主要代码如下: 源代码网推荐 源代码网推荐 源代码网推荐 源代码网推荐 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方法的代码比较简单: 源代码网推荐 源代码网推荐 源代码网推荐 源代码网推荐 GenerateSalt 源代码网推荐 internal string GenerateSalt() 源代码网推荐 { 源代码网推荐 byte[] data = new byte[0x10]; 源代码网推荐 new RNGCryptoServiceProvider().GetBytes(data); 源代码网推荐 return Convert.ToBase64String(data); 源代码网推荐 } 源代码网推荐 源代码网推荐 源代码网推荐 但是要注意的是,在这种方法里Salt值的高度随机性是安全的保障,所以不能简单的使用Random来获取随机数,而应该使用更安全的方式。这里使用了RNGCryptoServiceProvider来生成随机数。 源代码网推荐 源代码网推荐 EncodePassword方法的代码也不难: 源代码网推荐 源代码网推荐 源代码网推荐 源代码网推荐 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方法对密码进行检验。该方法有两种重载形式,最为完整的一种如下所示: 源代码网推荐 源代码网推荐 源代码网推荐 源代码网推荐 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等控件为我们带来的便利)。 源代码网推荐 源代码网推荐 做人要厚道,请注明转自酷网动力(www.ASPCOOL.COM)。 源代码网推荐 源代码网供稿. |
