ASP.NET中的窗体身份验证,不同的角色访问不同的目录

在《asp.net中的窗体身份验证(最简单篇)》说明了如何让通过了身份验证的用户访问网站,但是该方法中有一个缺点,就是访问整个网站都需要用户身份验证。因此,在《asp.net中的窗体身份验证(分目录验证篇)》中介绍了如何让通过了身份验证的用户访问特定的目录,例如网站根目录中的网页是任何用户都能访问的,而admin目录则只能让通过身份验证的用户访问。这种方法可以解决大部分网站的身份控制,但是,对于一些复杂的网站而言,这种方式就有点捉袊见肘了。例如,有一个网站,我们要求根目录中的网页可以让任何用户浏览,而admin目录中的网页只有管理员才能访问、backup目录中的网页只有管理员和系统备份员才能访问、user目录中的网页除了匿名用户之外的所有用户都能访问,对于这种情况,那要怎么办呢?是不是还可以使用Forms身份验证呢?

答案是肯定的,使用Forms身份验证可以通过角色的方式来达到上述目的。在本例中,我们可以先设定三种角色:admin、backup和reader,其中,admin角色也就是管理员角色,该角色可以访问admin目录、backup目录和user目录;backup角色也就是系统备份员角色,该角色不能访问admin目录,但可以访问backup目录和user目录;而reader角色是普通用户角色,该 角色不能访问admin目录、backup目录,只能访问user目录。

第一步、设置Forms身份验证方式和登录网页。

要达到上述目的,首先我们需要修改web.config文件,这次修改的目的是让网站拥有Forms身份验证功能(详见《asp.net中的窗体身份验证(最简单篇)》、《asp.net中的窗体身份验证(分目录验证篇)》和《asp.net中的窗体身份验证(完整篇之附录:web.config中相应节点详解)》),修改方式是:在<system.web>和</system.web>中找到<authentication>节,将其改为“<authentication mode="Forms"></authentication>”,其中Forms代表使用表单认证。

当然,你也可以在web.config文件中设置用户登录的网页,设置方法为:在<authentication mode="Forms">和</authentication>节点中添加“<forms loginUrl="~/login.aspx"></forms>”代码,其中loginUrl参数用于说明录页面,如果没有设置该参数的话,默认的登录网页就是网站根目录的login.aspx网页。完整的代码如下所示:

view plaincopy to clipboardprint?

<authentication mode="Forms"><!--验证方式为窗体验证-->

<forms loginUrl="~/login.aspx"></forms><!--登录网页为login.aspx-->

</authentication>

<authentication mode="Forms"><!--验证方式为窗体验证-->

<forms loginUrl="~/login.aspx"></forms><!--登录网页为login.aspx-->

</authentication>

第二步、设置默认的用户访问方式。

在本例中,我们要求是网站根目录中的网页可以让匿名用户访问,因此,在设置完Forms身份验证方式之后,我们还要设置默认的用户访问方式——也就是让所有的匿名用户访问。设置方式是:在<system.web>和</system.web>节点中添加以下代码:

view plaincopy to clipboardprint?

<authorization>

<allow users="?"/><!--默认为所有用户都能访问-->

</authorization>

<authorization>

<allow users="?"/><!--默认为所有用户都能访问-->

</authorization>

第三步、设置不同目录的访问方式。

在《asp.net中的窗体身份验证(分目录验证篇)》中,曾经介绍过分目录的验证方式,这种方式是在目录中创建一个web.config文件,然后在web.config文件中的<system.web>和</system.web>小节中,添加“<authorization></authorization>”节点,在该节点中设置不让匿名用户访问。在这里,我们介绍另外一种设置不同目录的访问方式,假设,我们现在要指定admin目录不能让匿名用户访问,那么,我们可以在根目录的web.config文件的<system.web>和</system.web>中添加以下代码:

view plaincopy to clipboardprint?

<location path="admin">

<system.web>

<authorization>

<!--拒绝所有匿名用户访问-->

<deny users="?"/>

</authorization>

</system.web>

</location>

<location path="admin">

<system.web>

<authorization>

<!--拒绝所有匿名用户访问-->

<deny users="?"/>

</authorization>

</system.web>

</location>

在以上代码中,<location>节点的path参数用于说明要对哪个目录可以访问控制,而<authorization>节点的作用同样是用于设置具体的访问权限。在以上代码中,<deny>节点用于说明拒绝哪些用户访问,如果需要拒绝所有用户访问,那么就要使用到“<deny users="*"/>”代码;如果需要允许所有用户访问,那么就需要使用“<allow users="*"/>”代码。然而,在<deny>节点和<allow>节点中还可以设置拒绝或允许哪些角色的用户访问,设置方法为“<deny roles="admin"/>”或“<allow roles="admin"/>”。其中,roles参数用于说明指定的拒绝或允许访问的角色名,如果存在多个角色,则用逗号进行分割。在本例中,要求admin目录只能让admin角色的用户访问、backup目录可以让admin角色和backup角色的用户访问,user目录可以让所有能过身份验证的用户访问(无论是什么角色),可以使用以下代码进行设置:

view plaincopy to clipboardprint?

<!--设置admin目录的访问权限-->

<location path="admin">

<system.web>

<authorization>

<!--允许角色为admin的用户可以访问-->

<allow roles="admin"/>

<!--拒绝所有其他用户访问-->

<deny users="*"/>

<!--注意以上两句的次序不能变-->

</authorization>

</system.web>

</location>

<!--设置backup目录的访问权限-->

<location path="backup">

<system.web>

<authorization>

<!--允许角色为admin和backup的用户可以访问-->

<allow roles="admin,backup"/>

<!--拒绝所有其他用户访问-->

<deny users="*"/>

<!--注意以上两句的次序不能变-->

</authorization>

</system.web>

</location>

<!--设置user目录的访问权限-->

<location path="user">

<system.web>

<authorization>

<!--拒绝所有匿名用户访问-->

<deny users="?"/>

</authorization>

</system.web>

</location>

<!--设置admin目录的访问权限-->

<location path="admin">

<system.web>

<authorization>

<!--允许角色为admin的用户可以访问-->

<allow roles="admin"/>

<!--拒绝所有其他用户访问-->

<deny users="*"/>

<!--注意以上两句的次序不能变-->

</authorization>

</system.web>

</location>

<!--设置backup目录的访问权限-->

<location path="backup">

<system.web>

<authorization>

<!--允许角色为admin和backup的用户可以访问-->

<allow roles="admin,backup"/>

<!--拒绝所有其他用户访问-->

<deny users="*"/>

<!--注意以上两句的次序不能变-->

</authorization>

</system.web>

</location>

<!--设置user目录的访问权限-->

<location path="user">

<system.web>

<authorization>

<!--拒绝所有匿名用户访问-->

<deny users="?"/>

</authorization>

</system.web>

</location>

第四步、添加登录页面。

由于在本例中,指定了网站目录中的login.aspx网页为登录面页,因此,在网站的根目录中添加一个login.aspx文件。然后在该文件中添加一个文本框(用于输入用户名)和一个密码框,再添加一个确定按钮。为了方便起见,我们假设用户名和密码都为1时,登录用户的角色为admin;用户名和密码都为2时,登录用户的角色为backup;用户名和密码都为3时,登录用户的角色为reader。登录网页中的代码如下所示:

view plaincopy to clipboardprint?

//用户名

string UserName = TextBox1.Text;

//密码

string UserPassword = TextBox2.Text;

//用户角色

string UserRole = "";

//用户身份验证

if ((UserName == "1" && UserPassword == "1") || (UserName == "2" && UserPassword == "2") || (UserName == "3" && UserPassword == "3"))

{

//判断用户权限

switch (UserName)

{

case "1":

UserRole = "admin";

break;

case "2":

UserRole = "backup";

break;

case "3":

UserRole = "reader";

break;

}

//创建一个身份验证票

FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(1, "yundao", DateTime.Now, DateTime.Now.AddMinutes(30), false, UserRole);

//将身份验证票加密

string EncrTicket = FormsAuthentication.Encrypt(ticket);

//创建一个Cookie

HttpCookie myCookie = new HttpCookie(FormsAuthentication.FormsCookieName, EncrTicket);

//将Cookie写入客户端

Response.Cookies.Add(myCookie);

//跳转到初始请求页或默认页面

Response.Redirect(FormsAuthentication.GetRedirectUrl("yundao", false));

}

//用户名

string UserName = TextBox1.Text;

//密码

string UserPassword = TextBox2.Text;

//用户角色

string UserRole = "";

//用户身份验证

if ((UserName == "1" && UserPassword == "1") || (UserName == "2" && UserPassword == "2") || (UserName == "3" && UserPassword == "3"))

{

//判断用户权限

switch (UserName)

{

case "1":

UserRole = "admin";

break;

case "2":

UserRole = "backup";

break;

case "3":

UserRole = "reader";

break;

}

//创建一个身份验证票

FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(1, "yundao", DateTime.Now, DateTime.Now.AddMinutes(30), false, UserRole);

//将身份验证票加密

string EncrTicket = FormsAuthentication.Encrypt(ticket);

//创建一个Cookie

HttpCookie myCookie = new HttpCookie(FormsAuthentication.FormsCookieName, EncrTicket);

//将Cookie写入客户端

Response.Cookies.Add(myCookie);

//跳转到初始请求页或默认页面

Response.Redirect(FormsAuthentication.GetRedirectUrl("yundao", false));

}

以上代码和前面介绍过的身份验证代码类似,惟一不同的是将用户角色写在了身体验证票的用户数据(也就是FormsAuthenticationTicket()方法的最后一个参数)中。

第五步、将用户角色添加到表示用户身份的GenericPrincipal对象中。

经过上面的步骤,我们已经将用户的角色存放在身份验证票据中了,然而,此时用于表示用户身份的GenericPrincipal对象中却还只是包含了用户信息而不包含用户的角色信息。在这一步骤中,我们必须将用户角色信息存放到GenericPrincipal对象中。至于怎么样将用户角色信息存放到GenericPrincipal对象中呢,我们就必须要先了解一下用户验证与授权的过程了:

1、客户端浏览器发送一个请求到服务器端。

2、服务器端接收到请求之后,会先检查该请求中是否包含有可能存在的恶意标记,如果存在恶意标记则会返回错误信息。

3、激活作为HTTP执行管线链中的第一个事件——BeginRequest事件。

4、处理完BeginRequest事件之后,asp.net将会创建用户标识,此时激发AuthenticateRequest事件,该事件可以确保事件处理程序之前对请求进行身份验证。

5、在AuthenticateRequest 事件之后,会继续触发PostAuthenticateRequest 事件,该事件同时是在asp.net创建用户标识时触发,但在该事件中可以访问由AuthenticateRequest事件中处理的任何数据。

6、然后,asp.net会进入用户授权验证,此时会激发AuthorizeRequest事件。

7、验证授权完毕之后,当用户获得了授权时,将会激发PostAuthorizeRequest事件。

8、到第7步为止,整个用户身份验证就已经完毕了,接下来将会激发PostResolveRequestCache事件,在该事件之后还会有N个事件,直到向客户端返回信息为止。但那件事件都不是本文的重点,也就不详细介绍了。

在用户登录确认身份之后,通常都会将身份验证票写到客户端的cookie中,然后客户端会将信息发送到服务器上,而在服务器端,asp.net会为每一个HTTP请求都分配一个HttpApplication对象,该对象的作用是处理HTTP请求的,也就是用来处理以上步骤的。从以上步骤中可以看出,身份验证是从AuthenticateRequest事件激发开始的,在该事件触发后,asp.net就会根据客户端发来的包含身份验证票的Cookie建立用户身份。

而asp.net创建的用户身份包含在HttpContext.User属性中,可以使用“HttpContext.Current.User”来获得得这个用户身份。对于Forms验证而言,HttpContext.User属性是一个GenericPrincipal对象,该对象有两个重要的属性:第一个是Identity属性,该属性存放的是当前用户的标识。Identity属性就是一个FormsIdentity对象,该对象的Name属性可以用户标识、Ticket属性可以获得用户的身份验证票。另外,GenericPrincipal对象还有一个m_role属性,这个属性是GenericPrincipal对象的私有属性,所以不能通过GenericPrincipal.m_role来访问,该属性是一个数组,用于存放用户角色。如果想知道用户是否属于某个角色,可以通过GenericPrincipal 对象的IsInRole()方法来判断。然而,由于身份验证票中并没有提供m_role参数,因此,在asp.net创建用户身份时,用户是没有角色信息的。换句话而言,对于Forms身份验证而言,在默认情况下,GenericPrincipal对象的m_role属性为空。

因此,如果想要使用角色的用户身份验证方式,就必须要在代表用户身份GenericPrincipal对象中添加用户角色。而且,这个工作必须要在第3个步骤中完成,因为第4个步骤的作用就是创建用户标识。因此,我们必须要在网站的根目录中创建一个Global.asax文件,Global.asax文件的作用就不用我再多说了了吧?

在Global.asax文件中添加代码“protected void Application_AuthenticateRequest(object sender, EventArgs e)”用于处理AuthenticateRequest事件。处理步骤如下所示:

1、首先判断用户的身份验证信息是否为空,如果HTTP请求中就没有用户身份验证信息,那么后面的步骤也就没必须进行了。前面介绍过,asp.net创建的用户身份包含在HttpContext.User属性中,因此,在该步骤中只需要判断HttpContext.User属性是否为空即可达到目的。

2、然后判断用户是否已经通过了身份验证,如果没有通过身份验证,那么在访问受限网页时,asp.net会自动跳转到登录网页上。前面介绍过,HttpContext.User属性也就是一个GenericPrincipal对象,该对象的Identity属性值为用户标识,那么只要判断Identity.IsAuthenticated是否为true,就可以知道用户是否已经通过身份验证。

3、由于asp.net的身份验证方式有多种,因此,在这里我们还要判断一下用户通过的身份验证方式是否为Forms验证方式。要达到该目的,只需要判断User.Identity是否为FormsIdentity对象即可。

4、然后,可以获得已经通过了Forms身份验证的用户标识,再从该用户标识的身份验证票中获得用户数据(也就是包含了用户角色的字符串)。

5、再然后,将用户角色字符串放到一个角色数组中——如果一个用户属于多个角色,通常我们都会将不同的角色使用逗号进行分割后放在用户数据中,然后将第4步骤中获得的用户数据分割后放在一个数组中,这样就得到了一个角色数组。

6、最后,创建一个新的GenericPrincipal对象,在创建该对象时将从第4步骤中获得的用户标识以及第5步骤中获得的角色数组一起放入GenericPrincipal对象,然后将该对象赋值给HttpContext.User。

这样,用户身份中就包含了角色信息。在随后的访问用,asp.net就会自动验证用户的角色以判断该用户是否可以访问限制的目录了。具体代码如下所示:

view plaincopy to clipboardprint?

protected void Application_AuthenticateRequest(object sender, EventArgs e)

{

//判断正在请求页的用户的身份验证信息是否为空

if (HttpContext.Current.User != null)

{

//判断用户是否已经进行了身份验证

if (HttpContext.Current.User.Identity.IsAuthenticated)

{

//判断当前用户身份验证的方式是否为Forms身份验证方式

if (HttpContext.Current.User.Identity is FormsIdentity)

{

//获得进行了Forms身份验证的用户标识

FormsIdentity UserIdent = (FormsIdentity)(HttpContext.Current.User.Identity);

//从身份验证票中获得用户数据

string UserData = UserIdent.Ticket.UserData;

//分割用户数据得到用户角色数组

string[] rolues = UserData.Split(',');

//从用户标识和角色组初始化GenericPrincipal类

HttpContext.Current.User = new System.Security.Principal.GenericPrincipal(UserIdent, rolues);

}

}

}

}

protected void Application_AuthenticateRequest(object sender, EventArgs e)

{

//判断正在请求页的用户的身份验证信息是否为空

if (HttpContext.Current.User != null)

{

//判断用户是否已经进行了身份验证

if (HttpContext.Current.User.Identity.IsAuthenticated)

{

//判断当前用户身份验证的方式是否为Forms身份验证方式

if (HttpContext.Current.User.Identity is FormsIdentity)

{

//获得进行了Forms身份验证的用户标识

FormsIdentity UserIdent = (FormsIdentity)(HttpContext.Current.User.Identity);

//从身份验证票中获得用户数据

string UserData = UserIdent.Ticket.UserData;

//分割用户数据得到用户角色数组

string[] rolues = UserData.Split(',');

//从用户标识和角色组初始化GenericPrincipal类

HttpContext.Current.User = new System.Security.Principal.GenericPrincipal(UserIdent, rolues);

}

}

}

}

至此,一个完整的带角色控制的窗体身份验证就已经做完了。