引言 前一段时间有两个朋友问我,为什么在HttpModule中无法获得到Session值,因为他们希望自定义一个HttpModule,然后在其中获取 Session来进行用户验证。我奇怪为什么不使用。Net Framework已经提供的验证机制,而要和Asp时一样,自己手工进行cookie+Session验证?我们是基于。Net Framework这个平台进行编程,所以我觉得,在很多情况下,使用Framework已经建立好的机制会显着地提高工作效率,而且。NET Framework内置的验证机制通常也更加安全。
.Net提供了一整套的验证和授权机制,这里验证和授权是不同的概念,验证(Authentication)是指“证明你确实是你所说的人”,通常是提供一个用户名和口令,然后与持久存储(比 如数据库)中的用户名和口令进行对比。授权(Authorization)是指“你是否有足够的权限做某件事”,此时你的身份已经被证明过了(匿名用户、 会员还是管理员),授权通常与用户组或者用户级别联系起来,不同的用户组拥有不同的权限(访问特定页面或者执行特定操作)。
回想一下我刚接触。Net时,也曾经完全绕过。NET的验证,自己编码采用Cookie+Session实现身份验证,并且一个Asp.Net 登录控件都没有使用,那时候的理由是:我要使用自定义的用户表,不能使用Asp.Net安全机 制在App_Data下自动生成的AspNetDB.mdf中的一系列数据表。除此以外,还有一个原因,就是。Net验证机制的核心IPrincipal 和Identity提供的信息用户信息太少了,当在页面后置代码中使用继承来的User属性(IPrincipal类型)时,它的Identity属性只 有一个Name与用户数据相关(AuthenticationType与IsAuthenticated都是与验证相关),而很多时候我们都需要许多额外 的用户数据。其实这只是一个误解罢了,以为使用Asp.Net的验证机制和登录控件就一定要使用其附带的数据表,以为Identity就只能携带一个 Name属性。
实际上,。NET的安全机制包括了几个部分,除了验证以外,还包括MemberShip、Profile、Role等,我们完全可以只使用它的验证机制, 而绕过它的MemberShip、Profile和Role,来实现通常我们用Cookie+Session完成的功能,而且更高效更安全。这篇文章将快 速地实现这样的一个流程。
开始前的准备
创建页面,配置Web.config
我们先创建解决方案、建立站点,然后在站点中添加下述文件,它们将会在后面使用:
接着对Web.config进行一下配置,首先看根目录下的Web.config:
<?xml version="1.0"?>
<configuration>
<system.web>
<authentication mode="Forms">
<forms timeout="600" slidingExpiration="true" loginUrl="~/SignIn.aspx" />
</authentication>
</system.web>
<location path="AuthOnly.aspx" >
<system.web>
<authorization>
<deny users="?" />
</authorization>
</system.web>
</location>
</configuration>
复制代码
这里我们指定了采用Forms验证,并且设置用户身份验证过期时间为600分钟(默认为30分钟),slidingExpiration的意思是说 timeout采用绝对时间还是滑动时间,当采用滑动时间时,如果在timeout时间内再次浏览页面,用户的最后活跃时间将设为当前时间,并重新开始计 算,这里我们采用滑动时间。loginUrl指定了登录页面,当匿名用户访问需要验证后才能访问的页面时,将会到自动导航到这里所设置的 SignIn.aspx页面,默认为Login.aspx。
接着我们指定AuthOnly.aspx页面为只有验证过的用户才可以访问。然后创建了AuthOnly文件夹,在其下添加了一个web.config,对这个目录进行设置,指定该文件夹下所有文件只允许验证用户进行访问。
<configuration>
<system.web>
<authorization>
<deny users="?" />
</authorization>
</system.web>
</configuration>
复制代码
创建用户数据表和数据访问
既然是用户登录,所以我们自然需要一张用户表,在App_Data下创建一个SiteData数据库,然后添加一张User用户表,表的设置如下:
这个表模拟了一个小型的论坛用户表,字段的含义基本都是自解释的,UserImage是用户头像的地址,PostCount是用户的发帖 数,ReplyCount是用户的回帖数,Level是用户的级别。我已经为表中添加了两条范例数据,其中一条用户名为JimmyZhang,密码为 password。
接下来我们需要添加一个存储过程,这个存储过程接收一个name参数,和一个password输出参数,根据name判断User表中是否存在该用户,如果存在,则由password带回正确的密码:
ALTER PROCEDURE dbo.IsValidUser
(
@userName varchar(50),
@password varchar(50) OUTPUT
)
AS
if Exists(Select Id From [User] Where [Name] = @userName)
Begin
Select @password = Password From [User] Where [name]= @userName
Select 1 -- Ture
End
Select 0 -- false
复制代码
这样做的目的是为了程序能够区分“不存在此用户”和“用户存在,但是密码不正确”这两种情况。如果Select的where子句为 [name]=@userName and [password] = @password,则无法进行区分。由数据库带回了正确的密码之后,我们只需要在程序中与用户输入的密码进行对比就可以知道用户的密码是否正确。
接下来我们创建一个强类型DataSet作为我们的数据访问层,因为我发现使用强类型DataSet作数据访问是最快的,基本不需要编写一行代码,在 App_Code中添加一个AuthDataSet数据集文件,然后将User表拖进去,另外配置一下UserTableAdapter,添加两个方法, 一个是GetUserTable(@name),它根据name参数获得用户信息;一个是IsValidUser(@userName, @password),它调用了上面的存储过程,并且返回一个标量值(0或者1)。
配置好以后,你的AuthDataSet应该和下面一样:
如果你查看一下生成的IsValidUser()方法,就会发现它具有这样的签名:
public virtual object IsValidUser(string userName, ref string password)
由于它返回的是一个object类型,并且接收的是一个ref参数,尽管这样最通用,但是可能不够方便,注意到UserTableAdapter是一个部 分类,所以我们可以在App_Code中再创建一个UserTableAdapter部分类,对它进行一个简单的包装:
namespace AuthDataSetTableAdapters {
// 检查是否是正确的用户名,如果是正确的用户名,带回正确的密码
public partial class UserTableAdapter {
public bool IsValidUserST(string userName, out string password) {
password = "";
return Convert.ToBoolean(this.IsValidUser(userName, ref password));
}
}
}
复制代码
这里的方法后缀ST,意思是StrongType(强类型)。好了,现在我们的数据访问就已经OK了,接下来我们看一下第一个页面:SignIn.aspx用户登录页面。
用户登录 -- 为Identity添加用户数据
Login.aspx页面实现
在登录页面,我们需要针对登录用户和非登录用户做不同的处理:如果用户尚未登录,则显示登录用的表单;如果用于已经登录了,则显示登录用户名并进行提示。完成这件事最好就是使用LoginView控件和LoginName控件了:
<asp:LoginView ID="LoginView1" runat="server">
<LoggedInTemplate>
<asp:LoginName ID="LoginName1" runat="server" />
,你已经登录了^_^ <br /><br />
你可以选择 <asp:LoginStatus ID="LoginStatus1" runat="server" LogoutPageUrl="~/Logout.aspx" LogoutAction="Redirect" />
</LoggedInTemplate>
<AnonymousTemplate>
用户名:<asp:TextBox ID="txtUserName" runat="server" Width="128px"></asp:TextBox>
<br />
密 码:<asp:TextBox ID="txtPassword" runat="server"></asp:TextBox>
<br />
<asp:Button ID="btnLogin" runat="server" Text="登 录" Width="100px" />
<br />
<br />
<asp:Label ID="lbMessage" runat="server" ForeColor="Red" Text=""></asp:Label>
</AnonymousTemplate>
</asp:LoginView>
复制代码
这里的关键是“登录”按钮的代码后置文件,在“引言”部分,我们提到了Identity中的信息太少,为了向Identity中添加信息,我们可以先获得 FormsIdentity的Ticket属性,它是一个FormsAuthenticationTicket类型,它含有一个UserData字符串属 性可以用于承载我们的用户数据,遗憾的是这个属性是只读的,为了给这个属性赋值,我们需要重新新构建一个 FormsAuthenticationTicket,并在构造函数中传入我们想要添加的用户信息。
FormasAuthenticationTicket包含了诸多用于用户验证的信息,它从Cookie中获得,可以认为它是服务端对Cookie的一个包装,只是这里的Cookie的操作不需要我们来处理,而由Asp.Net运行时去处理。具体的代码如下:
public partial class SignIn : System.Web.UI.Page {
private enum LoginResult {
Success,
UserNotExist,
PasswordWrong
}
// 用户登录
private LoginResult Login(string userName, string password) {
string validPassword; // 包含正确的密码
AuthDataSetTableAdapters.UserTableAdapter adapter =
new AuthDataSetTableAdapters.UserTableAdapter();
// 判断用户名是否正确
if (adapter.IsValidUserST(userName, out validPassword)) {
// 判断密码是否正确
if (password.Equals(validPassword))
return LoginResult.Success;
else
return LoginResult.PasswordWrong;
}
// 用户名不存在
return LoginResult.UserNotExist;
}
protected void btnLogin_Click(object sender, EventArgs e) {
TextBox txtUserName = LoginView1.FindControl("txtUserName") as TextBox;
TextBox txtPassword = LoginView1.FindControl("txtPassword") as TextBox;
Label lbMessage = LoginView1.FindControl("lbMessage") as Label;
string userName = txtUserName.Text;
string password = txtPassword.Text;
LoginResult result = Login(userName, password);
string userData = "登录时间" + DateTime.Now.ToString();
if (result == LoginResult.Success) {
SetUserDataAndRedirect(userName, userData);
} else if (result == LoginResult.UserNotExist) {
lbMessage.Text = "用户名不存在!";
}else {
lbMessage.Text = "密码有误!";
发表回复