.NET MVC4 SimpleMembershipProvider - 覆盖密码加密/存储?

时间:2013-03-06 20:06:03

标签: c# asp.net-mvc-4 asp.net-membership simplemembership

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

应用程序的当前生产版本有一个自定义MembershipProvider,它通过生成salt来加密密码并存储它们,使用salt(SHA256)散列密码,然后将salt存储为数据库存储密码的前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的实现?或者,手指交叉,是否有一些伏都教的简单方法我可以在不创建自定义会员资格提供者的情况下做到这一点?

谢谢!

2 个答案:

答案 0 :(得分:1)

我想我毕竟会走一条略有不同的路线:

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

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

感谢您的帮助。

答案 1 :(得分:1)

您正在寻找的是实施您自己的ExtendedMembershipProvider。似乎没有任何方法可以干扰SimpleMembershipProvider的加密方法,因此您需要编写自己的加密方法(例如PBKDF2)。我选择将salt与PBKDF2迭代一起存储在webpages_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);
    }
相关问题