`

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

阅读更多

本文英文原版及代码下载: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》

首先打开Membership文件夹里的UserBasedAuthorization.aspx页面,添加一个名为FilesGrid的GridView控件.在智能标签里点“Edit Columns”以打开Fields对话框,在对话框里不要点“Auto-generate fields” checkbox,然后,添加一个Select按钮、一个Delete按钮以及2个BoundFields(Select 和 Delete按钮位于CommandField下面)。设Select按钮的SelectText属性为“View”,在将第一个BoundField的HeaderText 和 DataField属性设为“Name”.将第二个BoundField的HeaderText设置为“Size in Bytes”,而将DataField属性设为“Length”,此外DataFormatString属性为“{0:N0}” ,HtmlEncode属性为False.

设置完后点OK关闭Fields对话框.在属性窗口里将GridView的DataKeyNames属性设置为FullName。这样,该GridView的声明代码就和下面的差不多:

<asp:GridView ID="FilesGrid" DataKeyNames="FullName" runat="server" AutoGenerateColumns="False">
<Columns>
<asp:CommandField SelectText="View" ShowSelectButton="True"/>
<asp:CommandField ShowDeleteButton="True" />
<asp:BoundField DataField="Name" HeaderText="Name" />
<asp:BoundField DataField="Length" DataFormatString="{0:N0}" HeaderText="Size in Bytes" HtmlEncode="False" />

</Columns>
</asp:GridView>

接下来,我们要写代码在某个文件夹里检索文件并绑定到GridView,在页面的Page_Load的事件处理器里添加如下的代码:
protected void Page_Load(object sender, EventArgs e)
{
if (!Page.IsPostBack)
{
string appPath = Request.PhysicalApplicationPath;
DirectoryInfo dirInfo = new DirectoryInfo(appPath);

FileInfo[] files = dirInfo.GetFiles();
FilesGrid.DataSource = files;
FilesGrid.DataBind();
}
}

上述代码使用DirectoryInfo class类来获取应用程序根目录下的文件列表.而GetFiles()方法将所有的文件以FileInfo objects的形式返回数组,然后绑定到GridView,该FileInfo object有诸如Name, Length, 以及IsReadOnly等的属性.如代码所示,GridView仅仅显示了其Name 和 Length属性.

注意:DirectoryInfo 和 FileInfo类属于System.IO命名空间.因此你在使用这些类时要么加命名空间前缀,要么将命名空间导入类文件(比如:using System.IO)

花点时间来测试页面,它将会将根目录下的文件显示出来,点“View” 或 “Delete”按钮都会产生页面回传,但不会真的发生什么动作,因为我们还没有创建相关的事件处理器.


图7:

我们需要将选中文件的内容显示出来.重新打开Visual Studio,在GridView上添加一个名为FileContents的TextBox.设其TextMode属性为MultiLine,Width和Rows属性分别为“95%”和10.

<asp:TextBox ID="FileContents" runat="server" Rows="10" TextMode="MultiLine" Width="95%">

</asp:TextBox>

接下来为GridView的SelectedIndexChanged事件添加如下的代码:

protected void FilesGrid_SelectedIndexChanged(object sender, EventArgs e)
{
// Open the file and display it
string fullFileName = FilesGrid.SelectedValue.ToString();
string contents = File.ReadAllText(fullFileName);
FileContents.Text = contents;
}

代码使用GridView的SelectedValue属性来判断所选文件的完整文件名.在内部,通过引用DataKeys来获得SelectedValue,因此我们务必要将GridView的 DataKeyNames属性设置为“Name”.我们使用File class类来将所选文件的内容读入一个字符串,再赋值给FileContents TextBox的Text属性,这样就将所选文件的内容显示在页面上了.


图8

注意:
如果你查看的内容包含HTML标记,然后尝试查看或删除一个文件话,你将收到一个HttpRequestValidationException错误.因为在回传时TextBox的内容将发回到web server,默认情况下,任何时候,当回传的内容存在潜在危险时,比如包含HTML标识,当检测到时就会引发一个HttpRequestValidationException错误.为避免这个报错,我们在页面的@Page声明里添加ValidateRequest="false".关于request validation的好处以及如何禁用它的更多详情请参阅文章《Request Validation – Preventing Script Attacks》

最后为GridView的RowDeleting事件添加如下的代码:

protected void FilesGrid_RowDeleting(object sender, GridViewDeleteEventArgs e)
{
string fullFileName = FilesGrid.DataKeys[e.RowIndex].Value.ToString();
FileContents.Text = string.Format("You have opted to delete {0}.", fullFileName);
// To actually delete the file, uncomment the following line
// File.Delete(fullFileName);
}

代码仅仅将要删除的文件的完整文件名显示在FileContents TextBox里,并没有真正的删除文件

图9


在第一步里我们禁止匿名用户访问Membership文件夹里的文件,为了更好的进行演示,我们允许所有的匿名用户访问UserBasedAuthorization.aspx页面,只是匿名用户访问时页面提供的功能有限.为了使该页面对所有的用户开放,在Membership文件夹里的Web.config文件里添加 <location>元素:

<location path="UserBasedAuthorization.aspx">
<system.web>
<authorization>
<allow users="*" />
</authorization>
</system.web>
</location>

添加<location>元素完毕后,我们注销后匿名登录,就可以访问UserBasedAuthorization.aspx页面了.

当前,任何的匿名和认证的用户都可以访问UserBasedAuthorization.aspx页面,并查看和删除文件。让我们使认证用户才可以查看内容,并只有Tito可以删除文件.要达到这样的目的,我们可以通过显式声明的方式或编程的方式,或2种方法结合的方式来达到目的.我们使用显式声明的方式限制用户对文件内容的查看,而使用编程的方式限制对文件的删除.

Using the LoginView Control

正如我们在前面的文章里看到的那样,当需要分别向匿名和认证用户显示不同的内容时LoginView控件是很有用的,并可以对匿名用户屏蔽掉某些功能.因为匿名用户无法看到或删除文件,因此我们只需要在认证用户访问时显示FileContents TextBox.为此,在页面里添加一个LoginView控件,id为LoginViewForFileContentsTextBox,再将FileContents TextBox的声明代码剪切到LoginView控件的LoggedInTemplate里.


<asp:LoginView ID=" LoginViewForFileContentsTextBox " runat="server">

<LoggedInTemplate>
<p>
<asp:TextBox ID="FileContents" runat="server" Rows="10" TextMode="MultiLine" Width="95%"></asp:TextBox>
</p>
</LoggedInTemplate>
</asp:LoginView>

LoginView的templates里的Web控件不能直接在后台代码里进行引用。比如,id为FilesGrid的GridView的SelectedIndexChanged 和 RowDeleting事件处理器直接引用FileContents TextBox,如下:

FileContents.Text = text;

这样做是无效的.因为放置到LoggedInTemplate里的FileContents TextBox是不能直接引用的,我们必须通过FindControl("controlId")方法来编程引用,对上面2个事件处理器进行更新,来这样进行引用:

TextBox FileContentsTextBox = LoginViewForFileContentsTextBox.FindControl("FileContents") as TextBox;

FileContentsTextBox.Text = text;

完成后,匿名访问该页面.如图10所示,FileContents TextBox就没有显示出来了,而“View” LinkButton依然可见.

图10


对匿名用户隐藏“View”按钮的一种方法是将其转换为一个TemplateField,这样将生成一个包含“View” LinkButton声明代码的模板.我们然后在模板里添加一个LoginView控件并将一个LinkButton放置在LoginView的LoggedInTemplate里,这样就达到了对匿名用户隐藏“View” 按钮的目的.为此,在GridView的智能标签里点“Edit Columns”打开Fields对话框,然后选中Select按钮,再点击“Convert this field to a

TemplateField” ,这样将使该字段的声明代码由<asp:CommandField SelectText="View" ShowSelectButton="True"/>改成
<asp:TemplateField ShowHeader="False">
<ItemTemplate>
<asp:LinkButton ID="LinkButton1" runat="server" CausesValidation="False" CommandName="Select" Text="View">
</asp:LinkButton>
</ItemTemplate>
</asp:TemplateField>

这时,我们可以向TemplateField添加一个LoginView,如下的代码使“View” LinkButton只对认证用户可见:

<asp:TemplateField ShowHeader="False">
<ItemTemplate>
<asp:LoginView ID="LoginView1" runat="server">
<LoggedInTemplate>
<asp:LinkButton ID="LinkButton1" runat="server" CausesValidation="False"

CommandName="Select" Text="View">
</asp:LinkButton>
</LoggedInTemplate>
</asp:LoginView>
</ItemTemplate>
</asp:TemplateField>

如图11所示,最终结果还不太完美,因为虽然“View” LinkButtons隐藏了但“View”这一列还是显示出来了.我们将在下一节看如何将这一列完全隐藏起来(而不仅是将LinkButton隐藏起来)


图11


Programmatically Limiting Functionality

在某些情况下,仅仅通过显式声明的方式来限制页面功能还是不够的.比如有些功能时否可用不是仅仅通过判断用户是匿名还是认证用户那么简单.在这种情况下,就要通过编程的方式来显示或隐藏各种用户界面了.为了通过编程的方式限制页面功能,我们要解决2个问题:

1.判断访问页面的用户是否可以使用某个功能

2.根据用户是否有权使用某功能来改变用户界面

为了对上述2个问题进行探讨,我们只允许Tito从GridView删除文件.第一个问题是判断当前用户是否是Tito,有了判断结果后再决定隐藏(或显示)GridView的Delete列.GridView的列可以通过其Columns属性来访问,只有当其Visible属性为true(默认值)时该列才会显示出来.

在将数据绑定到GridView前,先在Page_Load事件处理器里添加如下的代码:
// Is this Tito visiting the page?
string userName = User.Identity.Name;
if (string.Compare(userName, "Tito", true) == 0)
// This is Tito, SHOW the Delete column
FilesGrid.Columns[1].Visible = true;
else
// This is NOT Tito, HIDE the Delete column
FilesGrid.Columns[1].Visible = false;

如我们在前面的文章《An Overview of Forms Authentication》里探讨的那样,User.Identity.Name返回的时用户身份的名称.如果是Tito访问页面那么GridView的第二列的Visible属性就为true;否则就设置为false.最终结果是只要访问者不是Tito,比如匿名用户或其他认证用户,Delete列就不会显示出来(如图12);然而,如果是Tito访问则将Delete列显示出来(如图13).


图12

图13


第四步:Applying Authorization Rules to Classes and Methods

在第三步里我们禁止匿名用户访问一个文件的内容,同时只允许Tito删除文件.为此,我们同时使用了显式声明和编程2种技术来对未被授权的用户隐藏相应的用户界面.对本例而言,可能隐藏用户界面是直观易懂的,但对其他更复杂的站点而言又如何呢?它们可能有多种方式来执行相同的功能。对未授权的用户限制页面功能时,如果我们忘记隐藏或禁用所有的用户界面元素呢?

确保未授权的用户不能使用某个功能的一个简单办法是对该类或方法用PrincipalPermission特性进行修饰.当.NET runtime使用一个类或执行其中的一个方法时,它要检查以确保当前安全内容(current security context)有权使用某类或执行某个方法.PrincipalPermission提供了一种机制,通过该机制我们就可以定义规则.

让我们做个演示,对GridView的SelectedIndexChanged 和 RowDeleting事件处理器分别运用PrincipalPermission特性,分别禁止匿名用户和除Tito外的认证用户使用.我们要做的仅仅是分别在相应的函数定义上添加恰当的特性:

[PrincipalPermission(SecurityAction.Demand, Authenticated=true)]
protected void FilesGrid_SelectedIndexChanged(object sender, EventArgs e)
{ ... }

[PrincipalPermission(SecurityAction.Demand, Name="Tito")]
protected void FilesGrid_RowDeleting(object sender, GridViewDeleteEventArgs e)
{ ... }


为SelectedIndexChanged事件处理器添加的特性指出只有认证用户才可以执行该事件处理器;而RowDeleting事件处理器上添加的特性指出只有Tito才可以执行.

如果一个不是Tito的认证用户想执行RowDeleting事件处理器或一个匿名用户想执行SelectedIndexChanged事件处理器的话,.NET runtime将触发一个SecurityException异常.


图14


注意:
要允许多个security contexts访问一个类或方法,每个security contexts要用PrincipalPermission特性对类或方法进行修饰.打个比方,要允许Tito 和 Bruce执行RowDeleting事件处理器,要添加2个PrincipalPermission特性:

[PrincipalPermission(SecurityAction.Demand, Name="Tito")]
[PrincipalPermission(SecurityAction.Demand, Name="Bruce")]

除了ASP.NET页面外,很多应用程序都包括很多层,比如Business Logic 和Data Access层,这些层典型的都以类库(Class Libraries)的形式执行,包含了很多的类和方法来执行业务逻辑和数据相关的功能.要对这些层应用授权规则的话,PrincipalPermission特性是很有用的.运用PrincipalPermission特性对类和方法定义授权规则的更多详情,请参阅Scott Guthrie的博客《Adding Authorization Rules to Business and Data Layers Using PrincipalPermissionAttributes.》


结语:
本文我们考察如何应用基于用户的授权规则.我们首先考察了ASP.NET的URL authorization framework.对每个请求,ASP.NET引擎的UrlAuthorizationModule都会检查定义在配置文件里的URL authorization规则,以判断当前用户是否有权访问请求的资源.简单的说,URL authorization可以很方便地对某个页面或某个文件夹里的所有页面定义授权规则.

URL authorization framework是基于page-by-page的原则施加授权规则的.利用URL authorization,用户要么允许要么禁止访问某个具体的资源.很多情况下,我还需要更精细的授权规则,而不仅仅是判断哪些可以访问哪些不能访问某个页面.有时我们允许所有人访问页面,但是对不同的用户显示不同的数据和提供不同的功能.页面级的功能通常包括隐藏特定的用户界面元素以阻止未经授权的用户使用某种功能.此外,我们还可以使用一些特性来限制某些用户对类或方法的执行.

祝编程愉快!

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics