本文英文原版及代码下载:http://www.asp.net/learn/security/tutorial-14-cs.aspx
Security Tutorials系列文章第十四章:Unlocking and Approving User Accounts
导言:
用户帐户(user account)除了有username, password, 以及email等以外,还有2个状态字段用来确定该用户是否可以登陆站点:locked out 和 approved.如果在一段时间内,用户登陆失败的次数达到了一定的数字,该帐户就会被自动的锁定(默认是在10分钟内登陆失败5次),而approved状态在某些情况下很有用,比如在允许用户登陆站点前要先核实其邮件地址或要先获得管理员的批准后才能登陆站点。
由于锁定或未批准的帐户是无法登陆站点的,因此我们想知道如何重置这些状态值。ASP.NET没有包含内置的函数,或控件来管理用户的锁定或批准状态,部分原因在于每个站点的要求不一样,有些站点可能自动的批准用户帐户(这是默认的通行做法),而其它站点可能需要管理员来批准用户帐户等;同样的,某些站点可能要管理员对帐户解锁,而其它的站点则对锁定的帐户发送电子邮件,邮件里包含了一个URL,但他们访问该URL时才对帐户解锁。
本文我们探讨如何构建一个页面来供管理员管理用户的锁定或批准状态,我们也将考察如何在用户验证了其电邮地址的情况下才批准该新帐户。
Step 1: Managing Users’ Locked Out and Approved Statuses
在文章《Building an Interface to Select One User Account from Many》文章里我们构建了一个页面,在一个可分页的GridView控件里列出用户帐户,其列出了每个用户name、email,以及approved 和 locked out状态,是否在线等信息。要管理用户的approved 和 locked out状态,我们要使该控件可编辑。为了改变用户的approved status,管理员要首先确定要改动的用户帐户,再编辑对应的GridView row,通过选中或弃选checkbox来改变,还一种方法就是在另外一个页面里单独进行管理。
本文使用2个ASP.NET页面:ManageUsers.aspx 和 UserInformation.aspx.我们的想法是在ManageUsers.aspx页面里列出所有的用户帐户,而在UserInformation.aspx里使管理员可以对某个帐户的approved 和 locked out状态进行管理。我们首先要对ManageUsers.aspx页面里的GridView控件进行扩展,使其包含一个HyperLinkField,它将呈现为一个链接,我们想让每一个链接都指向UserInformation.aspx?user=UserName,其中UserName就是要编辑的帐户的帐户名。
注意:
如果你下载了《Recovering and Changing Passwords》文章的代码你会发现ManageUsers.aspx已经包含了一个系列的“Manage”链接,而UserInformation.aspx页面提供了一个接口供改变指定帐户的密码。而在本文的代码里我们明确的不会使用该功能,因为该功能绕开Membership API而直接操作数据库来改变用户的密码。
Adding “Manage” Links to the UserAccounts GridView
打开ManageUsers.aspx页面,向ID为UserAccounts的GridView控件里添加一个HyperLinkField.设置其Text属性为“Manage”,分别设置其DataNavigateUrlFields 和 DataNavigateUrlFormatString属性为UserName 和“UserInformation.aspx?user={0}”。这样一来,每个HyperLinkField都显示为“Manage”,但是每个链接都将相应的UserName值传递给查询字符串。
然后在浏览器里查看ManageUsers.aspx页面,如图1所示,每个GridView row都包含了一个“Manage”链接。比如,帐户Bruce对应的“Manage”链接指向的是UserInformation.aspx?user=Bruce,而帐户Dave指向的是UserInformation.aspx?user=Dave.
图1
我们先来看看如何编程改变用户的锁定或审核状态。MembershipUser类有IsLockedOut 和 IsApproved属性,其中IsLockedOut为只读的,没有办法通过编程的方式来锁定某个用户,要对锁定的帐户解锁,可以调用MembershipUser类的UnlockUser方法;而IsApproved属性则为可读写的。不管是解锁或审核用户,都要调用Membership类的UpdateUser方法,将修改后的MembershipUser对象传给它即可。
由于IsApproved属性为可读写的,我们可以在用户界面上放置一个CheckBox来控件该属性,而由于管理员可以对用户解锁而不能锁定,所以我们可以在用户界面上放置一个Button按钮,但点击该按钮时,可以对锁定的帐户解锁,而且只有但用户处于锁定状态时,该Button才能处于"可点击"的激活状态。
Creating the UserInformation.aspx Page
我们现在要在UserInformation.aspx页面里执行我们的user interface了,打开该页面,添加如下的控件:
.一个HyperLink控件:但点击它时,将管理员返回到ManageUsers.aspx页面
.一个Label控件:用于显示选定用户的name,设置其ID为UserNameLabel并清空其Text属性。
.一个CheckBox控件:其id为IsApproved,设置其AutoPostBack属性为true.
.一个Label控件:显示用户上次锁定的时间,设其id为LastLockedOutDateLabel,清空其Text属性。
.一个Button控件:用于对用户解锁,设其id为UnlockUserButton,而Text属性为“Unlock User”
.一个Label控件:用于显示状态信息,比如“The user’s approved status has been updated”,设置其id为StatusMessage,清空其Text属性,设置其CssClass属性为Important(该Important CSS是定义在Styles.css文件里的,它将相应的文字显示为大号,红色的).
添加完成后,在Visual Studio里界面看起来和下面的图2差不多:
图2
接下来我们就要根据选定的用户的信息来设置IsApproved CheckBox和其它控件了。在页面的Load事件里创建如下的事件处理器,添加如下的代码:
protected void Page_Load(object sender, EventArgs e)
{
if (!Page.IsPostBack)
{
// If querystring value is missing, send the user to ManageUsers.aspx
string userName = Request.QueryString["user"];
if (string.IsNullOrEmpty(userName))
Response.Redirect("ManageUsers.aspx");
// Get information about this user
MembershipUser usr = Membership.GetUser(userName);
if (usr == null)
Response.Redirect("ManageUsers.aspx");
UserNameLabel.Text = usr.UserName;
IsApproved.Checked = usr.IsApproved;
if (usr.LastLockoutDate.Year < 2000)
LastLockoutDateLabel.Text = string.Empty;
else
LastLockoutDateLabel.Text = usr.LastLockoutDate.ToShortDateString();
UnlockUserButton.Enabled = usr.IsLockedOut;
}
}
代码首先确保页面为第一次登陆,然后通过user查询字符串来获取帐户名称,再通过Membership.GetUser(username)来检索用户信息;如果查询字符串里没提供帐户名或找不到该帐户的信息,那么就导航到ManageUsers.aspx页面。
然后将MembershipUser对象的UserName值显示在UserNameLabel里,而IsApproved CheckBox控件的checked状态则取决于IsApproved属性的值。
MembershipUser对象的LastLockoutDate属性返回一个DateTime值,指出用户上次被锁定的时间,如果用户从来没有被锁定过,返回的值就取决于Membership provider了。但新帐户被创建时,SqlMembershipProvider将aspnet_Membership数据表的LastLockoutDate字段设置为SQL允许的最小的datetime值,也就是1754-01-01 12:00:00 AM,上面的代码是这样处理的:如果LastLockoutDate属性的值早于2000年,那么就返回空字符串,否则就将LastLockoutDate属性的值显示出来。而UnlockUserButton的Enabled属性设置为user的锁定状态(locked out status),意味着只有但该帐户被锁定时,该按钮才会处于激活状态。
来做测试,先登陆ManageUsers.aspx页面,选中一个帐户来管理,但导航到UserInformation.aspx页面时,注意,只有当该通过了帐户审核时,IsApproved CheckBox才处于选中状态,如果该用户被锁定了,则显示其被锁定时的时间,而且只有用户当前被锁定时,Unlock User按钮才会处于激活状态。不管是点击IsApproved CheckBox或Unlock User按钮,都会导致页面回传,不过我们现在还没写代码来处理这些事件。
在Visual Studiol里为IsApproved CheckBox的CheckedChanged event事件以及UnlockUser Button的Click event写事件处理器。在CheckedChanged事件处理器里,将用户的IsApproved属性设置为CheckBox的Checked属性,再调用Membership.UpdateUser方法来保存我们的修改。而在Click事件处理器里,仅仅调用MembershipUser对象的UnlockUser方法。在这2个事件处理器里,都要在StatusMessage Label里显示相应的消息。
protected void IsApproved_CheckedChanged(object sender, EventArgs e)
{
// Toggle the user's approved status
string userName = Request.QueryString["user"];
MembershipUser usr = Membership.GetUser(userName);
usr.IsApproved = IsApproved.Checked;
Membership.UpdateUser(usr);
StatusMessage.Text = "The user's approved status has been updated.";
}
protected void UnlockUserButton_Click(object sender, EventArgs e)
{
// Unlock the user account
string userName = Request.QueryString["user"];
MembershipUser usr = Membership.GetUser(userName);
usr.UnlockUser();
UnlockUserButton.Enabled = false;
StatusMessage.Text = "The user account has been unlocked.";
}
Testing the UserInformation.aspx Page
完成后,再次测试,改变用户的审核状态,如图3所示,你将看到一个简短的消息,指出该用户的IsApproved属性已经成功的修改了。
图3
接下来,注销并以刚才那个被修改为"未审核"的用户名登陆,由于该帐户未审核,因此不能登陆。默认时,不管是什么原因,Login控件都会显示一样的提示信息,不过在文章《Validating User Credentials Against the Membership User Store》里,我们看到了如何改进Login控件以显示更恰当的消息,如图4所示,消息显示由于帐户未通过审核,帐户Chris不能登陆系统。
图4
我们来测试锁定功能,以通过审核的帐户登陆,但输入错误的密码,但尝试登陆的次数达到可以让帐户锁定的次数为止。我们也对Login control进行了改动,当以锁定的帐户登陆时显示我们自己定义的错误消息,如果你看到这样的消息:“Your account has been locked out because of too many invalid login attempts. Please contact the administrator to have your account unlocked”时,你就知道你的帐户被锁定了。
返回ManageUsers.aspx页面,点击被锁定的帐户的对应的Manage链接,如图5所示,你将看到在LastLockedOutDateLabel里有一个锁定时的时间值,同时Unlock User按钮处于激活状态,点击该Unlock User按钮以对帐户解锁,这样该帐户又可以登陆系统了。
图5
Step 2: Specifying New Users’ Approved Status
审核状态在某些情况下是很有用的,比如:在新帐户可以登陆站点之前先执行某些操作或访问与具体用户相关的某些特性。打个比方,你正在管理一个个人站点,除了登陆和注册页面(signup page)外,所有的页面都只对授权用户开放,但是假如一个陌生人访问你的站点,发现了注册站点,创建了一个新帐户,那怎么办呢?要避免这样的事情发生,你可以将注册页面放到Administration文件夹,只允许管理员手动创建新帐户,或者你可以允许任何人注册,但要在管理员审核后才能登陆站点。
默认下,CreateUserWizard控件将自动审核通过新帐户,该行为可以通过LoginCreatedUser属性来控制,但设置为true时,新帐户将不会通过审核。
注意:
默认时CreateUserWizard控件将自动将新帐户登陆站点,我们可以通过属性LoginCreatedUser来控制该行为。由于未审核的用户是不能登陆站点的,当DisableCreatedUser为true时,新创建的用户帐户将不能登陆站点,不管LoginCreatedUser属性是什么值。
如果你是通过Membership.CreateUser方法以编程的方式创建新帐户的话,要想创建一个为审核的用户帐户,你可以使用接受这样的一个重载方法:它的参数里包含新帐户的IsApproved属性值。
Step 3: Approving Users By Verifying their Email Address
很多支持用户帐户的站点在用户提供电邮地址的情况才会对新帐户审核通过,这就需要唯一的,有效的地址,在注册过程里就多了一个步骤,使用这种模式的话,但新用户注册时,就要向用户发送一个邮件,邮件内容里包含一个导航到一个验证页面的链接,在访问该链接时就证明该用户收到了邮件,也自然说明用户提供的是有效的邮件地址,而验证页面的作用就在于通过新帐户的审核,该批准过程可以是自动的,只要用户到达该页面那就通过该新帐户的审核,或只有当用户提供某些其他信息时,比如一个CAPTCHA,才通过审核.
要使用这种流程,我们需要首先更新创建新帐户的页面,使新帐户"未通过审核",打开Membership文件夹里的EnhancedCreateUserWizard.aspx页面,设置CreateUserWizard控件的DisableCreatedUser属性为true.
接下来我们要配置CreateUserWizard控件向新帐户发送电邮,内容包含如何教导用户验证他的帐号。具体来说,我们将创建一个到Verification.aspx页面(我们到目前还没创建)的链接,在查询字符串里包含新帐户的UserId,而Verification.aspx页面将查找指定的帐户,通过对帐户的审核。
Sending a Verification Email to New Users
要使CreateUserWizard控件向用户发送电子邮件,只要恰当的设置其MailDefinition属性即可。就像在前面的文章探讨的那样,ChangePassword 和 PasswordRecovery控件都包含了一个MailDefinition属性,其用法和CreateUserWizard的一样。
注意:
要使用MailDefinition属性,你必须在Web.config配置文件里指定mail delivery项,更多信息请参阅《Sending Email in ASP.NET》
首先在EmailTemplates文件夹里创建一个名为CreateUserWizard.txt的新的邮件模板,使用如下的文本:
Hello <%UserName%>! Welcome aboard.
Your new acct is almost ready, but before you can login you must first visit:
<%VerificationUrl%>
Once you have visited the verification URL you will be redirected to the login page.
If you have any problems or questions, please reply to this email.
Thanks!
设置MailDefinition的BodyFileName属性为“~/EmailTemplates/CreateUserWizard.txt”,Subject属性为“Welcome to My Website! Please activate your account.”
注意到CreateUserWizard.txt邮件模板里有一个<%VerificationUrl%>占位符,它对应的是到Verification.aspx页面的相应URL,而CreateUserWizard自动的用新帐户的username 和password来取代<%UserName%> 和 <%Password%>占位符,不过由于没有内置的<%VerificationUrl%>占位符,所以我们要手动来将恰当的URL来取代该占位符。
为此,为CreateUserWizard控件的SendingMail event事件创建一个事件处理器,添加如下的代码:
protected void NewUserWizard_SendingMail(object sender, MailMessageEventArgs e)
{
// Get the UserId of the just-added user
MembershipUser newUser = Membership.GetUser(NewUserWizard.UserName);
Guid newUserId = (Guid)newUser.ProviderUserKey;
// Determine the full verification URL (i.e., http://yoursite.com/Verification.aspx?ID=...)
string urlBase = Request.Url.GetLeftPart(UriPartial.Authority) + Request.ApplicationPath;
string verifyUrl = "/Verification.aspx?ID=" + newUserId.ToString();
string fullUrl = urlBase + verifyUrl;
// Replace <%VerificationUrl%> with the appropriate URL and querystring
e.Message.Body = e.Message.Body.Replace("<%VerificationUrl%>", fullUrl);
}
SendingMail event事件发生在CreatedUser event事件之后,意味着当上面的代码创建了一个新的用户帐户后,我们可以这样来获得用户的UserId值:调用Membership.GetUser方法
,传入UserName值;接下来就要生成用于确认帐号的URL了。其中Request.Url.GetLeftPart(UriPartial.Authority)返回的是该URL的http://yourserver.com部分,而
Request.ApplicationPath返回的是该应用程序根目录的地址。再将代表Verification.aspx?ID=userId的verifyUrl字符串和urlBase字符串拼接起来形成最终的URL.最后,将邮件
内容里的<%VerificationUrl%>用该URL替换掉。
最终的结果是新创建的帐户未通过审核,自然不能登陆站点,此外,还自动的向用户发送一个电邮,电邮里包含了一个到verification URL的链接(如图6)
图6
注意:
CreateUserWizard控件的默认CreateUserWizard step显示一个消息提示用户,他们的帐户已经创建好了,并显示一个Continue按钮,当点击该按钮时, 将把用户导航到ContinueDestinationPageUrl属性所指定的URL。而在EnhancedCreateUserWizard.aspx页面里的CreateUserWizard控件被设置为将新用户导航到~/Membership/AdditionalUserInfo.aspx页面,该页面提示用户提供其hometown, homepage URL, 以及signature等信息.由于只有登陆了的用户才可以添加这些信息,因此我们要对该属性进行修改,以将用户导航到主页(~/Default.aspx),此外,要么对EnhancedCreateUserWizard.aspx页面进行修改,要么对CreateUserWizard控件进行扩展,以提示新用户,已经向他们发送了一个确认邮件,再依照邮件的步骤做以前他们是不能登陆站点的。我将这作为一个练习留给有兴趣的读者。
Creating the Verification Page
我们的最后一个任务是创建Verification.aspx页面,将该页面放在根目录下,套用Site.master母版页,就像我们在前面的文章里做的那样,去掉引用LoginContent ContentPlaceHolder的那个Content控件,这样当前用户才能使用母版页的默认版面。
在Verification.aspx页面上添加一个Label控件,设其id为StatusMessage,清空其text属性,接下来,创建Page_Load事件处理器,添加如下代码:
protected void Page_Load(object sender, EventArgs e)
{
if (string.IsNullOrEmpty(Request.QueryString["ID"]))
StatusMessage.Text = "The UserId was not included in the querystring...";
else
{
Guid userId;
try
{
userId = new Guid(Request.QueryString["ID"]);
}
catch
{
StatusMessage.Text = "The UserId passed into the querystring is not in the proper format...";
return;
}
MembershipUser usr = Membership.GetUser(userId);
if (usr == null)
StatusMessage.Text = "User account could not be found...";
else
{
// Approve the user
usr.IsApproved = true;
Membership.UpdateUser(usr);
StatusMessage.Text = "Your account has been approved. Please <a href=\"Login.aspx\">login</a> to the site.";
}
}
}
上述代码大意是对querystring提供的UserId进行验证,首先确保它是一个有效的Guid值,再确保它对应的是一个现有的用户帐户。如果这2项都通过则通过新帐户的审核,否则显示一个恰当的消息。
图7显示的是在浏览器里登陆时的Verification.aspx页面。
图7
结语:
所有Membership user accounts都有2个状态,用来判断该用户是否可以登陆站点:IsLockedOut以及 IsApproved.只有当这2个属性都为true时用户才可以登陆站点。
用户的locked out状态作为一种安全措施在防止黑客通过穷举法试图侵入系统时是很有用的。具体来说,但在一定的时间段内登陆失败的次数达到一定次数之后就会将该用户帐户锁定。而我们可以在Web.config.文件里对Membership provider进行配置来进行定制。
而approved状态是一种很常见的手段,为了在允许新帐户登陆站点之前执行一些行为。比如,站点可能需要管理员手动对新帐户进行审核,或就像我们在第3步里看到的那样,对用户提供的电子邮件地址进行验证。
祝编程愉快!