3

MVC4で再構築(グラウンドアップ)しようとしている本番アプリケーションがあります。SimpleMembershipProviderを認証と承認に使用することは、パスワードの暗号化という1つのことを除いて、私のニーズに非常に適しているようです。

アプリケーションの現在の製品版には、パスワードを暗号化し、ソルトを生成し、パスワードをソルト(SHA256)でハッシュし、データベースに保存されたパスワードの最初のX文字としてソルトを保存することでパスワードを保存するカスタムMembershipProviderがあります。

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

結局、私は少し異なるルートに行くつもりだと思います:

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

基本的に、レガシーユーザーデータをそのままUserProfileテーブルに移行し、SimpleMembership検証が失敗した場合に古いアルゴリズムに対して資格情報を検証するクラスを作成します。レガシー検証が成功した場合は、WebSecurity.ResetTokenを介してパスワードを新しいアルゴリズムに更新し、最新化します。

助けてくれてありがとう。

于 2013-03-07T04:42:24.420 に答える
1

あなたが探しているのは、あなた自身のExtendedMembershipProviderを実装することです。SimpleMembershipProviderの暗号化方式を妨害する方法はないようです。そのため、独自の方法(PBKDF2など)を作成する必要があります。私は、PBKDF2の反復とともにsaltを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);
    }
于 2013-09-18T23:14:08.703 に答える