ASP.NET 自定义成员资格提供程序 Part.2(实现提供程序类:XmlMembershipProvider)
本篇将完成 XmlMembershipProvider 类,它是自定义存储和成员资格 API 需求之间适配器的角色。
public class XmlMembershipProvider : System.Web.Security.MembershipProvider { }
每一个自定义的成员资格提供程序必须从这个类继承。(ASP.NET 4.0 中移到了 System.Web.ApplicationServices.dll 中)
需要实现很多属性和方法来满足成员资格 API 的需要。这些属性和方法用来查询、创建、更新、删除用户,以及获取提供程序的特定信息,比如密码要求等。这些属性会由安全控件来查询,比如 RequiresQuestionAndAnswer 属性由 CreateUserWizard 向导来查询。
先实现这些属性,这是最简单的部分。对于每一个属性,都应提供一个包含相应属性状态的私有变量。这些属性的含义与底层提供程序实现中的含义相同。属性中的大部分只有访问方法而没有设置方法。那么,ASP.NET 架构如何用 web.config 中配置的值来初始化这些属性的呢?
在所有提供程序的原始基类 System.Configuration.Provider.ProviderBase 中可以找到答案。重载它的 Initialize() 方法初始化属性的私有成员:
public override void Initialize(string name, System.Collections.Specialized.NameValueCollection config){}
现在一步步的实现 Xml的功能:
public override void Initialize(string name, System.Collections.Specialized.NameValueCollection config)
{
if (config == null)
{
throw new ArgumentNullException("config");
}
if (string.IsNullOrEmpty(name))
{
name = "XmlMembershipProvider";
}
if (string.IsNullOrEmpty(config["description"]))
{
config.Remove("description");
config.Add("description", "Xml Membership Provider");
}
base.Initialize(name, config);
......
}
首先,检查每一个配置是否都已被传入。如果没有为提供程序做任何配置,它不会工作。调用基类的初始化方法来适当的初始化基本的属性。接着,就开始初始化属性:
// Initialize default values
_ApplicationName = "DefaultApp";
_EnablePasswordReset = false;
_PasswordStrengthRegEx = @"[\w| !?%&/()=\-?\*]*";
_MaxInvalidPasswordAttempts = 3;
_MinRequiredNonAlphanumericChars = 1;
_MinRequiredPasswordLength = 5;
_RequiresQuestionAndAnswer = false;
_PasswordFormat = MembershipPasswordFormat.Hashed;
// Now go through the properties and initialize custom values
foreach (string key in config.Keys)
{
switch (key.ToLower())
{
case "name":
_Name = config[key];
break;
case "applicationname":
_ApplicationName = config[key];
break;
case "filename":
_FileName = config[key];
break;
case "enablepasswordreset":
_EnablePasswordReset = bool.Parse(config[key]);
break;
case "passwordstrengthregex":
_PasswordStrengthRegEx = config[key];
break;
case "maxinvalidpasswordattempts":
_MaxInvalidPasswordAttempts = int.Parse(config[key]);
break;
case "minrequirednonalphanumericchars":
_MinRequiredNonAlphanumericChars = int.Parse(config[key]);
break;
case "minrequiredpasswordlength":
_MinRequiredPasswordLength = int.Parse(config[key]);
break;
case "passwordformat":
_PasswordFormat = (MembershipPasswordFormat)Enum.Parse(
typeof(MembershipPasswordFormat), config[key]);
break;
case "requiresquestionandanswer":
_RequiresQuestionAndAnswer = bool.Parse(config[key]);
break;
}
}
这些代码初始化一些默认值,以防这些属性没有包含在 web.config 配置文件中。你甚至可以包含自定义设置,比如文件名设置 _FileName 属性(它不是成员资格提供程序的默认属性),它指向一个用来存储用户信息的 XML 文件。将这个文件名传递给 UserStore 类:
private UserStore CurrentStore
{
get
{
if (_CurrentStore == null)
{
_CurrentStore = UserStore.GetStore(_FileName);
}
return _CurrentStore;
}
}
在这个类的其余函数中,会一直用到这个属性访问数据存储。
提供程序中需要实现大量的方法,它们用来创建、更新和删除用户,以及访问并获取用户的详细信息。这些方法通过前面创建的存储类来访问这些信息。
1. 创建用户并将它们添加到存储
CreateUser() 方法需要确保用户名和 Email 地址是唯一的,而且密码是有效的并且符合密码强度的要求。
public override MembershipUser CreateUser(string username, string password,
string email, string passwordQuestion,
string passwordAnswer, bool isApproved,
object providerUserKey, out MembershipCreateStatus status)
{
try
{
// Validate the username and email
if (!ValidateUsername(username, email, Guid.Empty))
{
status = MembershipCreateStatus.InvalidUserName;
return null;
}
// Raise the event before validating the password
base.OnValidatingPassword(new ValidatePasswordEventArgs(username, password, true));
// Validate the password
if (!ValidatePassword(password))
{
status = MembershipCreateStatus.InvalidPassword;
return null;
}
// Everything is valid, create the user
SimpleUser user = new SimpleUser();
user.UserKey = Guid.NewGuid();
user.UserName = username;
user.PasswordSalt = string.Empty;
user.Password = this.TransformPassword(password, ref user.PasswordSalt);
user.Email = email;
user.PasswordQuestion = passwordQuestion;
user.PasswordAnswer = passwordAnswer;
user.CreationDate = DateTime.Now;
user.LastActivityDate = DateTime.Now;
user.LastPasswordChangeDate = DateTime.Now;
// Add the user to the store
CurrentStore.Users.Add(user);
CurrentStore.Save();
status = MembershipCreateStatus.Success;
return CreateMembershipFromInternalUser(user);
}
catch
{
throw;
}
}
刚开始,CreateUser() 调用了私有辅助方法 ValidateUsername 和 ValidatePassword,这些方法确定用户名和 Email 在存储中是唯一的,并确定密码符合要求。当这些检查成功后,就可以为底层存储(SimpleUser)创建用户了。
最后,这个方法需要返回 MembershipUser 的实例,并将其传递给调用它的 Membership 类,该实例包含了被创建的用户的详细信息。为此,只需要将 SimpleUser 实例的属性和 MembershipUser 的属性相匹配就可以了:
private MembershipUser CreateMembershipFromInternalUser(SimpleUser user)
{
MembershipUser muser = new MembershipUser(base.Name,
user.UserName, user.UserKey, user.Email, user.PasswordQuestion,
string.Empty, true, false, user.CreationDate, user.LastLoginDate,
user.LastActivityDate, user.LastPasswordChangeDate, DateTime.MaxValue);
return muser;
}
来看一下验证用户名、E-mail 地址和密码的方法:首先确认了密码的长度、接着用正则表达式确认密码中非字母数字字符的个数是否符合 MinRequiredNonAlphanumericCharacters 属性设置的要求,然后使用 PasswordStrengthRegularExpression 属性设置的正则表达式验证密码规范,通过这 3 项检查就返回 true。
private bool ValidateUsername(string userName, string email, Guid excludeKey)
{
bool IsValid = true;
UserStore store = UserStore.GetStore(_FileName);
foreach (SimpleUser user in store.Users)
{
if (user.UserKey.CompareTo(excludeKey) != 0)
{
if (string.Equals(user.UserName, userName, StringComparison.OrdinalIgnoreCase))
{
IsValid = false;
break;
}
if (string.Equals(user.Email, email, StringComparison.OrdinalIgnoreCase))
{
IsValid = false;
break;
}
}
}
return IsValid;
}
private bool ValidatePassword(string password)
{
bool IsValid = true;
Regex HelpExpression;
// Validate simple properties
IsValid = IsValid && (password.Length >= this.MinRequiredPasswordLength);
// Validate non-alphanumeric characters
HelpExpression = new Regex(@"\W");
IsValid = IsValid && (HelpExpression.Matches(password).Count >= this.MinRequiredNonAlphanumericCharacters);
// Validate regular expression
HelpExpression = new Regex(this.PasswordStrengthRegularExpression);
IsValid = IsValid && (HelpExpression.Matches(password).Count > 0);
return IsValid;
}
2. 登陆时验证用户
Membership 类提供了一个方法,可以通过程序验证用户输入的密码。这个方法也被 Login 控件使用。每一次用户试图登录,Membership.ValidateUser() 方法都会被涉及。
public override bool ValidateUser(string username, string password)
{
try
{
SimpleUser user = CurrentStore.GetUserByName(username);
if (user == null)
return false;
if (ValidateUserInternal(user, password))
{
user.LastLoginDate = DateTime.Now;
user.LastActivityDate = DateTime.Now;
CurrentStore.Save();
return true;
}
else
{
return false;
}
}
catch
{
throw;
}
}
这个方法从存储中获取用户信息,然后通过私有辅助方法 ValidateUserInternal 进行比较验证。如果用户名和密码都是正确的,就更新用户的 2 个相关日期值,并返回 true。
将密码验证功能单独打包到一个方法中会很有用,因为你的程序中可能不止一次会用到这个功能。一个非常典型的应用就是修改密码,你必需对旧密码进行验证。
public override bool ChangePassword(string username, string oldPassword, string newPassword)
{
try
{
// Get the user from the store
SimpleUser user = CurrentStore.GetUserByName(username);
if (user == null)
throw new Exception("User does not exist!");
if (ValidateUserInternal(user, oldPassword))
{
// Raise the event before validating the password
base.OnValidatingPassword(
new ValidatePasswordEventArgs(
username, newPassword, false));
if (!ValidatePassword(newPassword))
throw new ArgumentException("Password doesn't meet password strength requirements!");
user.PasswordSalt = string.Empty;
user.Password = TransformPassword(newPassword, ref user.PasswordSalt);
user.LastPasswordChangeDate = DateTime.Now;
CurrentStore.Save();
return true;
}
return false;
}
catch
{
throw;
}
}
3. 提供程序其余功能
初始化提供程序和创建并验证用户是提供程序中最重要也是最难实现的部分。其余的功能只是用来从数据存储中读取或者更新用户信息的。例如:
public override MembershipUser GetUser(string username, bool userIsOnline)
{
try
{
SimpleUser user = CurrentStore.GetUserByName(username);
if (user != null)
{
if (userIsOnline)
{
user.LastActivityDate = DateTime.Now;
CurrentStore.Save();
}
return CreateMembershipFromInternalUser(user);
}
else
{
return null;
}
}
catch
{
throw;
}
}