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

本文关键字:加密 存储 密码 覆盖 MVC4 SimpleMembershipProvider net | 更新日期: 2023-09-27 18:02:32

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

应用程序的当前生产版本有一个自定义MembershipProvider,它加密密码并通过生成盐(salt)来存储密码,用盐(SHA256)散列密码,然后将盐存储为数据库存储密码的前X个字符:

MyApp.Security。MyAppMembershipProvider:

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的实现吗?或者,手指交叉,是否有一些简单的方法,我可以做到这一点,而无需创建一个自定义的成员资格提供程序?

谢谢!

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

您正在寻找的是实现您自己的ExtendedMembershipProvider。似乎没有任何方法可以干扰SimpleMembershipProvider的加密方法,因此您需要编写自己的加密方法(例如PBKDF2)。我选择将盐与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);
    }

我想我还是要走一条稍微不同的路线:

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

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

谢谢你的帮助