. 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的实现吗?或者,手指交叉,是否有一些简单的方法,我可以做到这一点,而无需创建一个自定义的成员资格提供程序?
谢谢!
您正在寻找的是实现您自己的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使其现代化。
谢谢你的帮助