Security Tutorials系列文章第七章:User-Based Authorization(上)

来源:互联网 发布:巨杉数据库优势 编辑:程序博客网 时间:2024/06/05 06:26
 

本文英文原版及代码下载:http://www.asp.net/learn/security/tutorial-07-cs.aspx

Security Tutorials系列文章第七章:User-Based Authorization

    很多提供用户帐户的站点之所以要这样做,是为了对访问某些特定页面的访问着进行限制.以留言板程序为例,所有的人—不管是匿名的还是认证用户——都可以查看留言,但只有认证用户才能访问发表留言的页面.还有一些页面只有某个(某一小部分)用户才能访问.此外,一些页面级的功能会根据访问者的不同而不同.比如,当访问留言时,认证用户可以回复留言,而匿名用户则不允许回复.


在ASP.NET里我们可以很容易的定义基于用户的授权规则.只需要在Web.config文件里做些许定义,我们就能指定具体是哪些用户才可以访问某个页面或某个文件夹里的资源.同时,我们可以通过编程或显式声明的方式,根据当前登录的用户来打开或关闭页面级的功能.

   本文我们将看到多种技术来限制对页面的访问或约束页面级的功能.

A Look at the URL Authorization Workflow

   就像在前面的文章《An Overview of Forms Authentication》里探讨的一样,当ASP.NET runtime处理一个请求时,该请求在其声明周期里引发了很多的事件.而HTTP Modules就是来负责处理某个具体事件的,ASP.NET里有多个HTTP Module,用来在后台执行一些基本的任务.

   其中一个就是FormsAuthenticationModule. 就像在前面的文章里探讨的那样,它是用来探测当前请求的“身份”(identity)的,原理是检查窗体认证票据,当然,票据要么存储在cookie里要么内置在URL里.而对身份的探测工作是发生在AuthenticateRequest event事件里的.

另一个重要的HTTP Module就是UrlAuthorizationModule,其对应的事件是AuthorizeRequest event(该事件发生在AuthenticateRequest event事件之后)。该UrlAuthorizationModule对Web.config的配置情况进行检查,判断当前用户是否有权访问某个页面,该过程又称之为URL authorization.

   我们将在第一步里考察URL authorization规则.在此之前,我们先看看当请求经过授权或未经过授权时,UrlAuthorizationModule将会如何处理.如果UrlAuthorizationModule探测到请求经过了授权,那么什么都不做,请求继续进行.而如果请求未经授权,那么就打断请求,且使Response对象返回一个HTTP 401 Unauthorized状态.如果你使用的是窗体认证(forms authentication)的话HTTP 401是不会返回到客户端的,因为FormsAuthenticationModule会将HTTP 401状态修改为HTTP 302,直接将用户导航到登录页面.

   图1阐述了当一个未经授权的请求到达时,ASP.NET pipeline、 FormsAuthenticationModule、以及UrlAuthorizationModule的流程.具体来说,当一个匿名用户访问ProtectedPage.aspx时(该页面不允许匿名访问),由于是匿名用户,UrlAuthorizationModule打断请求,并返回一个未授权的HTTP 401状态,而接下来FormsAuthenticationModule又将HTTP 401状态转换为HTTP 302状态,直接导航到登录页面.当用户登录后,又被重新导航到ProtectedPage.aspx页面.此时,FormsAuthenticationModule就可以根据用户票据鉴别出用户了,自然UrlAuthorizationModule也就允许用户访问ProtectedPage.aspx页面了.


                                                                                   图1


    在上述情况下,匿名用户将被导航到登录页面,同时在查询字符串里保存了其要访问的那个页面,当匿名成功登录后,将自动的被重新导航到他想要访问的那个页面.当未经授权的请求是一个匿名用户发出的时候,该流程是直观的,用户也是很清楚是怎么一回事.但有一点要牢记:FormsAuthenticationModule会将任何未经授权的用户导航到登录页面,即使该用户已经“登录”了.这样的话,当一个已经"登录"的用户因为访问一个他无权访问的页面而被导航到登录页面时,那么这个已经"登录"的用户就感到很困惑了.

   我们来设想一下,我们的站点的URL authorization规则是这样定义的:OnlyTito.aspx页面只有用户Tito可以访问。假如一个名叫Sam的用户登录站点后,访问OnlyTito.aspx页面,那么UrlAuthorizationModule将打断请求,返回一个HTTP 401 Unauthorized状态,接下来FormsAuthenticationModule察觉到HTTP 401状态后直接将Sam导航到登录页面。Sam就觉得很奇怪了,还以为自己的认证信息莫名其妙的丢失了,再次登录后将会被导航到OnlyTito.aspx页面,而UrlAuthorizationModule再次探测到Sam无权访问该页面,这样Sam又再次的被导航到登录页面,如此反复.图2阐述了这种反复的过程


图2
我们将在第二步里探讨如何避免这种混乱的情况.

注意:
    ASP.NET使用2套机制来判断当前用户是否可以访问某个具体的web页面:URL authorization以及file authorization.而File authorization是由FileAuthorizationModule来贯彻的,它通过考察被请求文件的ACLs来判断授权情况.而且File authorization通常是在Windows authentication里面才使用,因为ACLs允许运用于Windows accounts.当使用forms authentication的时候,所有对操作系统和文件系统级别的请求都可以通过相同的Windows account来执行,而与访问站点的用户无关.由于本系列文章关注的是forms authentication,因此我们不会探讨file authorization.


The Scope of URL Authorization

UrlAuthorizationModule是ASP.NET runtime托管代码(managed code)的一部分,在微软的IIS7以前的版本,IIS的HTTP pipeline与ASP.NET runtime的pipeline之间有明显的区别. 简单的说,在IIS 6及更早版本里,当某个请求从IIS委托给ASP.NET runtime处理时,才会执行ASP.NET的UrlAuthorizationModule.默认情况下,IIS只处理静态的内容——比如HTML页面、CSS、JavaScript以及图片文件——只有当要请求的页面的后缀名为.aspx, .asmx,或.ashx时才将请求委托给ASP.NET runtime进行处理.

而IIS 7允许将IIS和ASP.NET的pipelines整合起来,通过少许设置,我们就可以使 IIS 7对所有的请求调用UrlAuthorizationModule,这就意味着可以对所有类型的文件定义URL authorization规则.此外,IIS 7还内置了自己的URL authorization引擎.更多信息可参阅文章《Understanding IIS7 URL Authorization》,更深入具体的探讨可看Shahram Khosravi的书《Professional IIS 7 and ASP.NET Integrated Programming》(ISBN: 978-0470152539).

   简单的说,在IIS 7以前的版本,只有当ASP.NET runtime处理资源请求时才会执行URL authorization.到了IIS 7,我们可以使用IIS自己的URL authorization特性或将ASP.NET的UrlAuthorizationModule整合进IIS的HTTP pipeline,这样就可以对所有请求使用URL authorization了.


第一步:Defining URL Authorization Rules in Web.config

   UrlAuthorizationModule根据应用程序的配置情况,对不同的用户允许或禁止对某个资源的访问.具体的授权规则在<authorization>元素里以<allow> 和 <deny>子元素的形式定义.每一个<allow> 和 <deny>子元素可以指定为:

.一个具体的用户
.一连串的用户,用逗号隔开
.所有的匿名用户,用(?)表示
.所有的注册用户,用(*)表示

下面的代码显示允许Tito和Scott,而禁止其他的用户:
<authorization>     
<allow users="Tito, Scott" />    
<deny users="*" />
</authorization>

其中<allow>元素定义了允许的用户——Tito 和 Scott;而<deny>元素定义了禁止的用户——所有的用户

注意:
    <allow> 和 <deny>元素也可以对role定义规则,我们将在后面的文章考察基于role的授权

下面的设置允许所有的用户访问,除了Sam(当然还包括匿名用户):

<authorization>     
   <deny users="Sam" />
</authorization>

要对所有通过认证的用户授权,禁止所有匿名用户可这样来配置:
<authorization>    
<deny users="?" />
</authorization>

   授权规则在Web.config文件的<system.web>元素里定义,适用于站点的所有资源.不过在有些时候,对不同的节点要运用不同的授权规则,以一个电子商务网站来说,所有的访客都可以查看所有的产品,产品预览,查找产品目录等,另外只有登录用户才可以转到某个页面管理自己的购物清单.此外,站点的某些地方只允许特点的用户,比如站点管理员才能访问.

在ASP.NET里我们可以很方便的对站点里的不同文件夹和不同文件定义不同的授权规则.在根目录的Web.config文件里定义的授权规则适用于整个站点资源,不过我们可以在某个文件夹下再添加一个Web.config文件,在<authorization>节点里指定授权规则,以重写根目录下的授权规则.

   让我们来做改动,规定只有通过认证的用户才可以访问Membership文件夹里的页面.为此,在Solution Explorer里,右击Membership文件夹,选择“Add New Item”,添加一个新的名为Web.config的Web Configuration File.

图3

此时我们的项目里就有2个Web.config文件了,一个在根目录下,一个在Membership文件夹下.

图4
对Membership文件夹下的配置文件做改动,禁止匿名用户访问.

<?xml version="1.0"?>
<configuration>     
<system.web>         
   <authorization>               
     <deny users="?" />          
   </authorization>     
</system.web>
</configuration>

就这么简单!

来测试,访问主页且不要登录站点,因为我们没有对根目录的Web.config进行改动,默认是允许匿名用户访问根目录的.再点左边的“Creating User Accounts”链接,这将使你访问~/Membership/CreatingUserAccounts.aspx页面.而我们在Membership文件夹的Web.config文件里定义了禁止匿名用户访问,因此UrlAuthorizationModule将打断请求并返回一个HTTP 401 Unauthorized状态,接下来FormsAuthenticationModule又将该状态修改为302 Redirect状态,将用户导航的登录页面.注意,我们希望访问的页面(CreatingUserAccounts.aspx)储存在登录页面的ReturnUrl查询字符串里.

图5

一旦成功登录我们将被导航回CreatingUserAccounts.aspx页面,此时UrlAuthorizationModule允许我们对该页面的访问,因为我不是匿名用户了.

Applying URL Authorization Rules to a Specific Location


   根目录下的Web.config文件的<system.web>节点的授权设置适用于根目录的所有资源以及子目录(当然,子目录里的Web.config文件可以对授权规则重写).一般来说,某个目录下的所有资料都用同一种授权规则,不过在某些情况下,我们希望对该目录下的几个页面应用另一种授权规则,此时我们可以在Web.config文件里添加一个<location>元素,指向要单独施加授权规则的文件,并定义授权规则.

举个例子,比如我们想规定只有Tito可以访问CreatingUserAccounts.aspx页面.为此,我们在Membership文件夹里的Web.config文件里添加一个<location>元素,做如下的修改:

<?xml version="1.0"?> <configuration>   
<system.web>         
    <authorization>              
      <deny users="?" />         
    </authorization>    
</system.web>    
<location path="CreatingUserAccounts.aspx">   
     <system.web>              
       <authorization>                 
         <allow users="Tito" />               
         <deny users="*" />           
       </authorization>         
     </system.web>    
</location>
</configuration>

其中<system.web>节点里的<authorization>元素为Membership文件夹及其子文件夹里的ASP.NET资源定义了默认的URL authorization规则.而<location>元素允许对某个资源重写这些默认的规则,比如上面的<location>元素针对CreatingUserAccounts.aspx
指定了授权规则,只允许Tito访问该页面.

来验证,匿名访问站点,如果你访问Membership文件夹里的任何一个页面的话,比如UserBasedAuthorization.aspx,那么UrlAuthorizationModule会禁止你访问并把你重导航到登录页面.再以注册会员的名义,比如Scott,登录站点,你就可以访问Membership文件夹里除CreatingUserAccounts.aspx以外的所有页面.当你访问CreatingUserAccounts.aspx的话,同样会被导航到登录页面.

注意:
    <location>元素必须位于<system.web>节点之外,对每个你希望单独定义授权规则的页面,你必须在一个<location>元素里指向它.

A Look at How the UrlAuthorizationModule Uses the Authorization Rules to Grant or Deny Access

每次处理请求时,UrlAuthorizationModule都要判断是否授权某个用户访问某个资源.只要找到相应的授权规则,就根据规则同意或禁止用户访问资源,当然这取决于是否在<allow>或<deny>元素里找到匹配的规则.注意:如果没有找到规则,那么用户是可以访问资源的. 如果你想限制访问,那么在最后你至少要使用一个<deny>元素进行限制,如果你忽略掉<deny>元素的话,所有的用户都可以进行访问.

为了更好的理解UrlAuthorizationModule判断是否授权的流程,我们来考察前面定义的授权规则.第一个规则用<allow>元素来允许Tito 和 Scott访问;第二个规则用一个<deny>元素来禁止所有人访问.如果一个匿名用户访问,那么UrlAuthorizationModule首先就要看“当前用户是Scott 或 Tito吗?”,明显,答案为No,接着在看第二条规则:“所有人包括匿名用户吗?”,当然包括!,因此<deny>规则就适用了,将匿名用户导航到登录页面.类似的,如果Jisun来访,UrlAuthorizationModule最开始就考察:“当前用户是Scott 或 Tito吗?”,不是!因此,再看第二条规则:“所有人包括Jisun吗?”,当然包括!,因此禁止Jisun访问,并将Jisun导航到登录页面.最后,如果是Tito来访,UrlAuthorizationModule发现第一条规则适用于Tito,因此Tito就可以访问页面了.

由于UrlAuthorizationModule处理授权规则是按从上到下进行的,如果发现某条规则适用就不用再考察余下的规则了,因此很重要的一点就是:过滤条件多的规则在前,过滤条件少的在后.比如,我们要指定这样的授权规则,禁止Jisun以及所有的匿名用户访问,允许除Jisun外的所有认证用户访问,你应该首先定义过滤条件多的规则——也就是禁止Jisun访问,接下来再定义过滤条件少的规则——允许所有的认证用户,禁止所有的匿名用户.下面的规则就达到了这个目的,首先禁止Jisun,再禁止所有的匿名用户.任何一个认证用户,只要不是Jisun就可以访问资源,因为这2条<deny>规则都不适用:

<authorization>    
<deny users="Jisun" />     
<deny users="?" />
</authorization>


第二步:Fixing the Workflow for Unauthorized, Authenticated Users

就像我们在前面的“A Look at the URL Authorization Workflow” 部分探讨的那样,任何时候,只要请求是未授权的,UrlAuthorizationModule就会返回一个HTTP 401 Unauthorized的状态,该401状态又会被FormsAuthenticationModule修改为302 Redirect状态,将用户导航到登录页面,哪怕用户以及登录站点了.

将已经登录的用户转到登录页面会使用户感到很困惑.我们只需少许修改就可以改进,当通过认证的用户访问为授权的页面时将他们导航到一个页面,指出他们无权访问某个页面.

首先,在根目录下添加一个名为UnauthorizedAccess.aspx的页面,选用母版页Site.master.然后移除包含LoginContent ContentPlaceHolder的Content控件,以显示母版页的默认内容,然后添加一个消息说明用户试图访问一个受限的资源,如此,UnauthorizedAccess.aspx的声明代码和下面的差不多:

<%@ Page Language="C#" MasterPageFile="~/Site.master" AutoEventWireup="true" CodeFile="UnauthorizedAccess.aspx.cs" Inherits="UnauthorizedAccess" Title="Untitled Page" %>
<asp:Content ID="Content1" ContentPlaceHolderID="MainContent" Runat="Server">    
<h2>Unauthorized Access</h2>     
<p>          
You have attempted to access a page that you are not authorized to view.      </p>    
<p>          
If you have any questions, please contact the site administrator.    
</p>
</asp:Content>

我们现在需要对流程进行改动,如果认证用户的请求未被授权,就将认证用户导航到UnauthorizedAccess.aspx页面而不是登录页面.将未授权的请求导航到登录页面是由FormsAuthenticationModule类的一个private方法负责处理的,因此我们不能对该处理逻辑进行定制.我们能做的是,如果必要的话,对登录页面添加我们自己的处理逻辑,将认证用户从登录页面再导航到UnauthorizedAccess.aspx页面.

当FormsAuthenticationModule将未授权的用户导航到登录页面时,它会做一些处理,将受限的unauthorized URL存储在名为ReturnUrl的字符串里.比如,一个未授权的用户访问OnlyTito.aspx页面时,FormsAuthenticationModule会将用户导航到Login.aspx?ReturnUrl=OnlyTito.aspx,因此,当未被授权的用户被导航到登录页面,且查询字符串包含ReturnUrl参数时,我们就知道用户刚才试图访问一个他无权访问的页面,此时我们就可以将用户导航到UnauthorizedAccess.aspx页面.

为此,我们在登录页面的Page_Load事件处理器里添加如下的代码:

protected void Page_Load(object sender, EventArgs e)
{      if (!Page.IsPostBack)    
   {         
if (Request.IsAuthenticated && !string.IsNullOrEmpty(Request.QueryString["ReturnUrl"]))              
// This is an unauthorized, authenticated request...                Response.Redirect("~/UnauthorizedAccess.aspx");     
   }
}

上述代码将通过认证的,但未授权的用户导航到UnauthorizedAccess.aspx页面.我们来测试一下,以匿名访问站点,点击左边的“Creating User Accounts” 链接.对应的是~/Membership/CreatingUserAccounts.aspx页面,我们在第一步里配置为只有Tito才能访问.由于匿名用户无法访问,FormsAuthenticationModule就会把你导航会登录页面.

此时,由于我们是匿名用户,因此Request.IsAuthenticated返回false,因此我们就不会被导航到UnauthorizedAccess.aspx页面,而是停留在登录页面.以Tito以外的名义登录站点,比如Bruce,那么登录后我们会被导航回~/Membership/CreatingUserAccounts.aspx页面,然而,该页面只有Tito有权访问,我们未被授权,再次被导航回登录页面,不过此时Request.IsAuthenticated返回true(并且存在ReturnUr查询字符串参数) ,因此我们又被导航到了UnauthorizedAccess.aspx页面.

图6

该自定义工作流给人更直观更易理解的用户体验


第三步:Limiting Functionality Based on the Currently Logged In User

   URL authorization比较笼统地指定了授权规则,正如我们在第一步里用URL authorization规定了哪些用户可以访问某些资源,哪些又不能访问.不过在某些时候对某个页面,我们允许所有的用户都可以访问,只是根据当前的不同而限制某些功能.

以一个电子商务网站为例.当一个匿名用户访问某个产品的页面时,他只能看到产品信息,但不能对产品发表评论.而如果是一个通过认证的用户访问该页面的话就可以看到发表留言的方框.再进行一下扩展,该网站的员工访问该页面时还可以看到其它的信息,比如该产品的库存量,允许修改产品的价格和描述等.

要么通过显式要么通过编程的方式来执行这种授权规则(或者2者结合起来使用)都可以.在下一节,我们将看如何使用LoginView控件来达到该目的,到时我们将考察使用编程的方式.不过我们首先要创建一个根据当前登录的用户而提供不同功能的页面.

让我们创建一个页面,将某个目录里的文件信息显示在一个GridView控件里,列出每个文件的name, size等信息.该GridView控件将包含二个LinkButton列,一个的标题时“View”,而另一个的是“Delete”.如果点击“View”的话就将该文件的内容显示出来,而如果点击“Delete” 的话就删除该文件.最开始该这2个功能对所有用户而言都是可用的,在后面的“Using the LoginView Control” 和 “Programmatically Limiting Functionality” 部分,我们将看到如何根据当前用户的不同而启用或禁用这些功能.

注意:
    我们将创建的这个ASP.NET页面用一个GridView控件来显示信息.由于本文的焦点是forms authentication, authorization, user accounts,以及roles,我不打算花太多时间来描述GridView的内部工作原理.对GridView的详细探究,请看我以前的的系列文章《Working with Data in ASP.NET 2.0》

原创粉丝点击