3

我有一个生产应用程序,我希望在 MVC4 上重新构建(基础)。使用 SimpleMembershipProvider 进行身份验证和授权似乎非常适合我的需求,除了一件事:密码加密。

该应用程序的当前生产版本有一个自定义 MembershipProvider 加密密码并通过生成盐来存储它们,使用盐 (SHA256) 对密码进行哈希处理,然后将盐存储为数据库存储密码的前 X 个字符:

MyApp.Security.MyAppMembershipProvider:System.Web.Security.MembershipProvider:

public override MembershipUser CreateUser(string username, string password, string email, string passwordQuestion, string passwordAnswer, bool isApproved, object providerUserKey, out MembershipCreateStatus status) {

    // ...

    u.Email = email.ToLower();

    string salt = GenerateSalt();
    u.Password = salt + Helper.FormatPassword(salt, password, this.PasswordFormat);
    u.FirstName = String.Empty;
    u.LastName = String.Empty;

    // ...

}

当我将应用程序转换为 MVC4 时,明显的问题是我希望我的用户的旧密码继续对他们进行身份验证。我愿意迁移到新的数据架构,但旧的身份验证信息需要继续工作。

我的问题是,是否可以使用 SimpleMembershipProvider 以相同的方式覆盖?我是否必须使用 ExtendedMembershipProvider 的实现?或者,交叉手指,是否有一些巫毒教简单的方法可以在不完全创建自定义会员提供程序的情况下做到这一点?

谢谢!

4

2 回答 2

1

您正在寻找的是实现您自己的 ExtendedMembershipProvider。似乎没有任何方法可以干扰 SimpleMembershipProvider 的加密方法,因此您需要自己编写(例如 PBKDF2)。我选择将 salt 与 PBKDF2 迭代一起存储在 pages_Membership 的 PasswordSalt 列中,这样您就可以在以后计算机变得更快并即时升级旧密码时增加此值。

这样的模板示例可能如下所示:

    using WebMatrix.Data;
    using WebMatrix.WebData;
    using SimpleCrypto;
    public class CustomAuthenticationProvider : ExtendedMembershipProvider
    {
        private string applicationName = "CustomAuthenticationProvider";
        private string connectionString = "";
        private int HashIterations = 10000;
        private int SaltSize = 64;

        public override void Initialize(string name, System.Collections.Specialized.NameValueCollection config)
        {
            try
            {
                if (config["connectionStringName"] != null)
                    this.connectionString = ConfigurationManager.ConnectionStrings[config["connectionStringName"]].ConnectionString;
            }
            catch (Exception ex)
            {
                throw new Exception(String.Format("Connection string '{0}' was not found.", config["connectionStringName"]));
            }
            if (config["applicationName"] != null)
                this.connectionString = ConfigurationManager.ConnectionStrings[config["applicationName"]].ConnectionString;

            base.Initialize(name, config);
        }

        public override bool ConfirmAccount(string accountConfirmationToken)
        {
            return true;
        }

        public override bool ConfirmAccount(string userName, string accountConfirmationToken)
        {
            return true;
        }

        public override string CreateAccount(string userName, string password, bool requireConfirmationToken)
        {
            throw new NotImplementedException();
        }

        public override string CreateUserAndAccount(string userName, string password, bool requireConfirmation, IDictionary<string, object> values)
        {
            // Hash the password using our currently configured salt size and hash iterations
            PBKDF2 crypto = new PBKDF2();
            crypto.HashIterations = HashIterations;
            crypto.SaltSize = SaltSize;
            string hash = crypto.Compute(password);
            string salt = crypto.Salt;

            using (SqlConnection con = new SqlConnection(this.connectionString))
            {
                con.Open();
                int userId = 0;
                // Create the account in UserProfile
                using (SqlCommand sqlCmd = new SqlCommand("INSERT INTO UserProfile (UserName) VALUES(@UserName); SELECT CAST(SCOPE_IDENTITY() AS INT);", con))
                {
                    sqlCmd.Parameters.AddWithValue("UserName", userName);
                    object ouserId = sqlCmd.ExecuteScalar();
                    if (ouserId != null)
                        userId = (int)ouserId;
                }
                // Create the membership account and associate the password information
                using (SqlCommand sqlCmd = new SqlCommand("INSERT INTO webpages_Membership (UserId, CreateDate, Password, PasswordSalt) VALUES(@UserId, GETDATE(), @Password, @PasswordSalt);", con))
                {
                    sqlCmd.Parameters.AddWithValue("UserId", userId);
                    sqlCmd.Parameters.AddWithValue("Password", hash);
                    sqlCmd.Parameters.AddWithValue("PasswordSalt", salt);
                    sqlCmd.ExecuteScalar();
                }
                con.Close();
            }
            return "";
        }

        public override bool ChangePassword(string username, string oldPassword, string newPassword)
        {
            // Hash the password using our currently configured salt size and hash iterations
            PBKDF2 crypto = new PBKDF2();
            crypto.HashIterations = HashIterations;
            crypto.SaltSize = SaltSize;
            string oldHash = crypto.Compute(oldPassword);
            string salt = crypto.Salt;
            string newHash = crypto.Compute(oldPassword);

            using (SqlConnection con = new SqlConnection(this.connectionString))
            {
                con.Open();
                con.Close();
            }
            return true;
        }

        public override bool ValidateUser(string username, string password)
        {
            bool validCredentials = false;
            bool rehashPasswordNeeded = false;
            DataTable userTable = new DataTable();

            // Grab the hashed password from the database
            using (SqlConnection con = new SqlConnection(this.connectionString))
            {
                con.Open();
                using (SqlCommand sqlCmd = new SqlCommand("SELECT m.Password, m.PasswordSalt FROM webpages_Membership m INNER JOIN UserProfile p ON p.UserId=m.UserId WHERE p.UserName=@UserName;", con))
                {
                    sqlCmd.Parameters.AddWithValue("UserName", username);
                    using (SqlDataAdapter adapter = new SqlDataAdapter(sqlCmd))
                    {
                        adapter.Fill(userTable);
                    }
                }

                con.Close();
            }

            // If a username match was found, check the hashed password against the cleartext one provided
            if (userTable.Rows.Count > 0)
            {
                DataRow row = userTable.Rows[0];

                // Hash the cleartext password using the salt and iterations provided in the database
                PBKDF2 crypto = new PBKDF2();
                string hashedPassword = row["Password"].ToString();
                string dbHashedPassword = crypto.Compute(password, row["PasswordSalt"].ToString());

                // Check if the hashes match
                if (hashedPassword.Equals(dbHashedPassword))
                    validCredentials = true;

                // Check if the salt size or hash iterations is different than the current configuration
                if (crypto.SaltSize != this.SaltSize || crypto.HashIterations != this.HashIterations)
                    rehashPasswordNeeded = true;
            }

            if (rehashPasswordNeeded)
            {
                // rehash and update the password in the database to match the new requirements.
                // todo: update database with new password
            }

            return validCredentials;
        }
}

加密类如下(在我的例子中,我使用了一个名为SimpleCrypto的 PBKDF2 加密包装器):

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;

namespace SimpleCrypto
{

    /// <summary>
    /// 
    /// </summary>
    public class PBKDF2 : ICryptoService
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="PBKDF2"/> class.
        /// </summary>
        public PBKDF2()
        {
            //Set default salt size and hashiterations
            HashIterations = 100000;
            SaltSize = 34;
        }

        /// <summary>
        /// Gets or sets the number of iterations the hash will go through
        /// </summary>
        public int HashIterations
        { get; set; }

        /// <summary>
        /// Gets or sets the size of salt that will be generated if no Salt was set
        /// </summary>
        public int SaltSize
        { get; set; }

        /// <summary>
        /// Gets or sets the plain text to be hashed
        /// </summary>
        public string PlainText
        { get; set; }

        /// <summary>
        /// Gets the base 64 encoded string of the hashed PlainText
        /// </summary>
        public string HashedText
        { get; private set; }

        /// <summary>
        /// Gets or sets the salt that will be used in computing the HashedText. This contains both Salt and HashIterations.
        /// </summary>
        public string Salt
        { get; set; }


        /// <summary>
        /// Compute the hash
        /// </summary>
        /// <returns>
        /// the computed hash: HashedText
        /// </returns>
        /// <exception cref="System.InvalidOperationException">PlainText cannot be empty</exception>
        public string Compute()
        {
            if (string.IsNullOrEmpty(PlainText)) throw new InvalidOperationException("PlainText cannot be empty");

            //if there is no salt, generate one
            if (string.IsNullOrEmpty(Salt))
                GenerateSalt();

            HashedText = calculateHash(HashIterations);

            return HashedText;
        }


        /// <summary>
        /// Compute the hash using default generated salt. Will Generate a salt if non was assigned
        /// </summary>
        /// <param name="textToHash"></param>
        /// <returns></returns>
        public string Compute(string textToHash)
        {
            PlainText = textToHash;
            //compute the hash
            Compute();
            return HashedText;
        }


        /// <summary>
        /// Compute the hash that will also generate a salt from parameters
        /// </summary>
        /// <param name="textToHash">The text to be hashed</param>
        /// <param name="saltSize">The size of the salt to be generated</param>
        /// <param name="hashIterations"></param>
        /// <returns>
        /// the computed hash: HashedText
        /// </returns>
        public string Compute(string textToHash, int saltSize, int hashIterations)
        {
            PlainText = textToHash;
            //generate the salt
            GenerateSalt(hashIterations, saltSize);
            //compute the hash
            Compute();
            return HashedText;
        }

        /// <summary>
        /// Compute the hash that will utilize the passed salt
        /// </summary>
        /// <param name="textToHash">The text to be hashed</param>
        /// <param name="salt">The salt to be used in the computation</param>
        /// <returns>
        /// the computed hash: HashedText
        /// </returns>
        public string Compute(string textToHash, string salt)
        {
            PlainText = textToHash;
            Salt = salt;
            //expand the salt
            expandSalt();
            Compute();
            return HashedText;
        }

        /// <summary>
        /// Generates a salt with default salt size and iterations
        /// </summary>
        /// <returns>
        /// the generated salt
        /// </returns>
        /// <exception cref="System.InvalidOperationException"></exception>
        public string GenerateSalt()
        {
            if (SaltSize < 1) throw new InvalidOperationException(string.Format("Cannot generate a salt of size {0}, use a value greater than 1, recommended: 16", SaltSize));

            var rand = RandomNumberGenerator.Create();

            var ret = new byte[SaltSize];

            rand.GetBytes(ret);

            //assign the generated salt in the format of {iterations}.{salt}
            Salt = string.Format("{0}.{1}", HashIterations, Convert.ToBase64String(ret));

            return Salt;
        }

        /// <summary>
        /// Generates a salt
        /// </summary>
        /// <param name="hashIterations">the hash iterations to add to the salt</param>
        /// <param name="saltSize">the size of the salt</param>
        /// <returns>
        /// the generated salt
        /// </returns>
        public string GenerateSalt(int hashIterations, int saltSize)
        {
            HashIterations = hashIterations;
            SaltSize = saltSize;
            return GenerateSalt();
        }

        /// <summary>
        /// Get the time in milliseconds it takes to complete the hash for the iterations
        /// </summary>
        /// <param name="iteration"></param>
        /// <returns></returns>
        public int GetElapsedTimeForIteration(int iteration)
        {
            var sw = new Stopwatch();
            sw.Start();
            calculateHash(iteration);
            return (int)sw.ElapsedMilliseconds;
        }


        private string calculateHash(int iteration)
        {
            //convert the salt into a byte array
            byte[] saltBytes = Encoding.UTF8.GetBytes(Salt);

            using (var pbkdf2 = new Rfc2898DeriveBytes(PlainText, saltBytes, iteration))
            {
                var key = pbkdf2.GetBytes(64);
                return Convert.ToBase64String(key);
            }
        }

        private void expandSalt()
        {
            try
            {
                //get the position of the . that splits the string
                var i = Salt.IndexOf('.');

                //Get the hash iteration from the first index
                HashIterations = int.Parse(Salt.Substring(0, i), System.Globalization.NumberStyles.Number);

            }
            catch (Exception)
            {
                throw new FormatException("The salt was not in an expected format of {int}.{string}");
            }
        }


    }
}

如果没有界面,它就不会完整:

public interface ICryptoService
    {
        /// <summary>
        /// Gets or sets the number of iterations the hash will go through
        /// </summary>
        int HashIterations { get; set; }

        /// <summary>
        /// Gets or sets the size of salt that will be generated if no Salt was set
        /// </summary>
        int SaltSize { get; set; }

        /// <summary>
        /// Gets or sets the plain text to be hashed
        /// </summary>
        string PlainText { get; set; }

        /// <summary>
        /// Gets the base 64 encoded string of the hashed PlainText
        /// </summary>
        string HashedText { get; }

        /// <summary>
        /// Gets or sets the salt that will be used in computing the HashedText. This contains both Salt and HashIterations.
        /// </summary>
        string Salt { get; set; }

        /// <summary>
        /// Compute the hash
        /// </summary>
        /// <returns>the computed hash: HashedText</returns>
        string Compute();

        /// <summary>
        /// Compute the hash using default generated salt. Will Generate a salt if non was assigned
        /// </summary>
        /// <param name="textToHash"></param>
        /// <returns></returns>
        string Compute(string textToHash);

        /// <summary>
        /// Compute the hash that will also generate a salt from parameters
        /// </summary>
        /// <param name="textToHash">The text to be hashed</param>
        /// <param name="saltSize">The size of the salt to be generated</param>
        /// <param name="hashIterations"></param>
        /// <returns>the computed hash: HashedText</returns>
        string Compute(string textToHash, int saltSize, int hashIterations);

        /// <summary>
        /// Compute the hash that will utilize the passed salt
        /// </summary>
        /// <param name="textToHash">The text to be hashed</param>
        /// <param name="salt">The salt to be used in the computation</param>
        /// <returns>the computed hash: HashedText</returns>
        string Compute(string textToHash, string salt);

        /// <summary>
        /// Generates a salt with default salt size and iterations
        /// </summary>
        /// <returns>the generated salt</returns>
        string GenerateSalt();

        /// <summary>
        /// Generates a salt
        /// </summary>
        /// <param name="hashIterations">the hash iterations to add to the salt</param>
        /// <param name="saltSize">the size of the salt</param>
        /// <returns>the generated salt</returns>
        string GenerateSalt(int hashIterations, int saltSize);

        /// <summary>
        /// Get the time in milliseconds it takes to complete the hash for the iterations
        /// </summary>
        /// <param name="iteration"></param>
        /// <returns></returns>
        int GetElapsedTimeForIteration(int iteration);
    }
于 2013-09-18T23:14:08.703 回答
1

我想我毕竟会走一条稍微不同的路线:

http://pretzelsteelersfan.blogspot.com/2012/11/migrating-legacy-apps-to-new.html

基本上,如果 SimpleMembership 验证失败,则将旧用户数据按原样迁移到 UserProfile 表并创建一个类来验证旧算法的凭据。如果遗留验证成功,则通过 WebSecurity.ResetToken 将密码更新为新算法以使其现代化。

谢谢您的帮助。

于 2013-03-07T04:42:24.420 回答