基于用户的身份验证 (C#)

作者 :斯科特·米切尔

注意

自本文撰写以来,ASP.NET 成员身份提供程序已被 ASP.NET 标识取代。 强烈建议更新应用以使用 ASP.NET 标识 平台,而不是本文撰写时精选的成员资格提供程序。 ASP.NET 标识在 ASP.NET 成员身份系统中具有许多优势,包括:

  • 性能更好
  • 改进了可扩展性和可测试性
  • 支持 OAuth、OpenID Connect 和双因素身份验证
  • 基于声明的标识支持
  • 与 ASP.Net Core 更好的互操作性

下载代码下载 PDF

在本教程中,我们将探讨如何通过各种技术限制对页面的访问和限制页面级功能。

介绍

大多数提供用户帐户的 Web 应用程序部分用于限制某些访问者访问网站中的某些页面。 例如,在大多数联机邮件板网站中,所有用户(匿名和经过身份验证)都可以查看邮件板的帖子,但只有经过身份验证的用户才能访问网页以创建新文章。 并且可能只有特定用户(或特定用户集)才能访问管理页面。 此外,页面级功能可能会因用户而异。 查看帖子列表时,经过身份验证的用户会显示每个帖子的分级界面,而此界面对匿名访问者不可用。

ASP.NET 可以轻松定义基于用户的授权规则。 只有一点标记 Web.config,特定的网页或整个目录可以锁定,以便只能访问指定的用户子集。 可以通过编程和声明性方式基于当前登录的用户打开或关闭页面级功能。

在本教程中,我们将探讨如何通过各种技术限制对页面的访问和限制页面级功能。 现在就开始吧!

查看 URL 授权工作流

如窗体身份验证概述教程中所述,当 ASP.NET 运行时处理 ASP.NET 资源的请求时,请求在其生命周期内会引发许多事件。 HTTP 模块 是托管类,其代码是在响应请求生命周期中的特定事件时执行的。 ASP.NET 附带许多 HTTP 模块,这些模块在后台执行基本任务。

一个这样的 HTTP 模块是 FormsAuthenticationModule。 如前面的教程中所述,主要功能 FormsAuthenticationModule 是确定当前请求的标识。 这是通过检查表单身份验证票证来实现的,该票证位于 Cookie 中或嵌入在 URL 中。 此标识在 AuthenticateRequest 事件期间发生。

另一个重要的 HTTP 模块是在UrlAuthorizationModule响应事件时引发的AuthorizeRequest(事件发生后AuthenticateRequest)。 检查 UrlAuthorizationModule 配置标记 Web.config ,以确定当前标识是否有权访问指定的页面。 此过程称为 URL 授权

我们将检查步骤 1 中 URL 授权规则的语法,但首先让我们看看具体操作 UrlAuthorizationModule ,具体取决于请求是否获得授权。 UrlAuthorizationModule如果确定请求已授权,则它不执行任何操作,并且请求会在其生命周期内继续。 但是,如果请求 获得授权,则 UrlAuthorizationModule 中止生命周期并指示 Response 对象返回 HTTP 401 未 授权状态。 使用表单身份验证时,不会将 HTTP 401 状态返回到客户端,因为如果 FormsAuthenticationModule 检测到 HTTP 401 状态将它修改为 HTTP 302 重定向 到登录页。

图 1 说明了 ASP.NET 管道的工作流, FormsAuthenticationModule以及 UrlAuthorizationModule 未经授权的请求到达时间。 具体而言,图 1 显示了匿名访问者 ProtectedPage.aspx的请求,该请求是拒绝匿名用户访问的页面。 由于访问者是匿名的, UrlAuthorizationModule 因此中止请求并返回 HTTP 401 未授权状态。 然后,将 FormsAuthenticationModule 401 状态转换为 302 重定向到登录页。 通过登录页对用户进行身份验证后,他将被重定向到 ProtectedPage.aspx。 这一次,根据 FormsAuthenticationModule 用户的身份验证票证标识用户。 对访问者进行身份验证后,允许 UrlAuthorizationModule 访问页面。

表单身份验证和 URL 授权工作流

图 1:表单身份验证和 URL 授权工作流(单击可查看全尺寸图像

图 1 描述了匿名访问者尝试访问匿名用户不可用的资源时发生的交互。 在这种情况下,匿名访问者将重定向到登录页,其中包含她尝试访问的查询字符串中指定的页面。 用户成功登录后,她将自动重定向回最初尝试查看的资源。

匿名用户发出未经授权的请求时,此工作流非常简单,访问者可以轻松了解所发生的情况和原因。 但请记住,FormsAuthenticationModule将任何未经授权的用户重定向到登录页,即使请求是由经过身份验证的用户发出的。 如果经过身份验证的用户尝试访问她缺少权限的页面,这可能会导致用户体验混乱。

假设网站配置了其 URL 授权规则,以便 ASP.NET 页面 OnlyTito.aspx 只能访问 Tito。 现在,假设山姆访问该网站,登录,然后尝试访问 OnlyTito.aspx。 该 UrlAuthorizationModule 操作将停止请求生命周期并返回 HTTP 401 未授权状态, FormsAuthenticationModule 该状态将检测到该状态,然后将 Sam 重定向到登录页。 不过,由于 Sam 已经登录,她可能想知道她为什么被发回登录页。 她可能会因为某种原因而丢失登录凭据,或者输入了无效凭据。 如果 Sam 从登录页重新输入凭据,她将登录(再次)并重定向到 OnlyTito.aspx。 将 UrlAuthorizationModule 检测到 Sam 无法访问此页面,她将返回到登录页。

图 2 描述了这种令人困惑的工作流。

默认工作流可能导致混乱的周期

图 2:默认工作流可能导致混乱的周期(单击可查看全尺寸图像

图 2 中所示的工作流可以快速混淆,即使是最精明的计算机访问者也是如此。 我们将探讨在步骤 2 中防止这种令人困惑的周期的方法。

注意

ASP.NET 使用两种机制来确定当前用户是否可以访问特定网页:URL 授权和文件授权。 文件授权由该文件 FileAuthorizationModule实现,它通过咨询请求的文件 ACL 来确定颁发机构。 文件授权最常用于Windows 身份验证,因为 ACL 是适用于 Windows 帐户的权限。 使用表单身份验证时,无论访问站点的用户如何,所有操作系统和文件系统级请求都由同一 Windows 帐户执行。 由于本教程系列重点介绍表单身份验证,因此我们不会讨论文件授权。

URL 授权的范围

UrlAuthorizationModule ASP.NET 运行时的一部分的托管代码。 在 Microsoft Internet Information Services (IIS) Web 服务器版本 7 之前,IIS 的 HTTP 管道与 ASP.NET 运行时的管道之间存在明显的障碍。 简言之,在 IIS 6 及更早版本中,ASP。 UrlAuthorizationModule 只有在将请求从 IIS 委托给 ASP.NET 运行时时,NET 才会执行。 默认情况下,IIS 处理静态内容本身(如 HTML 页面、CSS、JavaScript 和图像文件),并且仅在扩展名为.aspx.asmx.ashx请求的页面时将请求移交给 ASP.NET 运行时。

但是,IIS 7 允许集成 IIS 和 ASP.NET 管道。 通过一些配置设置,可以设置 IIS 7 来调用UrlAuthorizationModule所有请求,这意味着可以为任何类型的文件定义 URL 授权规则。 此外,IIS 7 还包括自己的 URL 授权引擎。 有关 ASP.NET 集成和 IIS 7 的本机 URL 授权功能的详细信息,请参阅 了解 IIS7 URL 授权。 若要更深入地了解 ASP.NET 和 IIS 7 集成,请选取 Shahram Khosravi 的书籍、 专业 IIS 7 和 ASP.NET 集成编程 (ISBN: 978-0470152539) 的副本。

简言之,在 IIS 7 之前的版本中,URL 授权规则仅适用于 ASP.NET 运行时处理的资源。 但是,使用 IIS 7 时,可以使用 IIS 的本机 URL 授权功能或集成 ASP。NET 进入 UrlAuthorizationModule IIS 的 HTTP 管道,从而将此功能扩展到所有请求。

注意

ASP 的方式存在一些微妙但重要的差异。NET 和 UrlAuthorizationModule IIS 7 的 URL 授权功能处理授权规则。 本教程不检查 IIS 7 的 URL 授权功能,也不检查它分析授权规则与该规则的区别 UrlAuthorizationModule。 有关这些主题的详细信息,请参阅有关 MSDN 或 www.iis.net 的 IIS 7 文档。

步骤 1:定义 URL 授权规则Web.config

确定 UrlAuthorizationModule 是基于应用程序配置中定义的 URL 授权规则授予还是拒绝对特定标识的请求资源的访问权限。 授权规则以 <authorization> 元素 的形式 <allow><deny> 子元素的形式进行拼写。 每个 <allow> 元素和 <deny> 子元素都可以指定:

  • 特定用户
  • 以逗号分隔的用户列表
  • 所有匿名用户,用问号表示(?)
  • 所有用户,由星号表示 \

以下标记演示了如何使用 URL 授权规则允许用户 Tito 和 Scott 并拒绝所有其他内容:

<authorization>
 <allow users="Tito, Scott" />
 <deny users="*" />
</authorization>

<allow> 元素定义允许哪些用户 - Tito 和 Scott - 而 <deny> 该元素指示 所有用户 被拒绝。

注意

这些 <allow><deny> 元素还可以为角色指定授权规则。 我们将在未来的教程中检查基于角色的授权。

以下设置授予对 Sam 以外的任何人(包括匿名访问者)的访问权限:

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

若要仅允许经过身份验证的用户,请使用以下配置,拒绝访问所有匿名用户:

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

授权规则在元素Web.config<system.web>定义,并应用于 Web 应用程序中的所有 ASP.NET 资源。 通常,应用程序对不同部分有不同的授权规则。 例如,在电子商务网站上,所有访问者都可以使用产品、查看产品评论、搜索目录等。 但是,只有经过身份验证的用户才能访问结帐或页面来管理其发货历史记录。 此外,某些网站的某些部分只能由选择用户访问,例如网站管理员。

ASP.NET 可以轻松地为站点中的不同文件和文件夹定义不同的授权规则。 根文件夹 Web.config 文件中指定的授权规则适用于站点中的所有 ASP.NET 资源。 但是,可以通过添加包含<authorization>节的一个Web.config文件夹来重写这些默认授权设置。

让我们更新网站,以便只有经过身份验证的用户才能访问文件夹中的 ASP.NET 页面 Membership 。 为此,我们需要将 Web.config 文件添加到 Membership 文件夹,并设置其授权设置以拒绝匿名用户。 右键单击Membership解决方案资源管理器中的文件夹,从上下文菜单中选择“添加新项”菜单,然后添加新的 Web 配置文件。Web.config

将 Web.config 文件添加到成员资格文件夹

图 3:将 Web.config 文件添加到 Membership 文件夹(单击可查看全尺寸图像

此时,项目应包含两 Web.config 个文件:一个在根目录中,一个在 Membership 文件夹中。

应用程序现在应包含两个 Web.config 文件

图 4:应用程序现在应包含两 Web.config 个文件(单击以查看全尺寸图像

更新文件夹中的 Membership 配置文件,使其禁止访问匿名用户。

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

就是这么简单!

若要测试此更改,请访问浏览器中的主页,并确保已注销。由于 ASP.NET 应用程序的默认行为是允许所有访问者,并且由于我们没有对根目录 Web.config 的文件进行任何授权修改,因此我们可以以匿名访问者身份访问根目录中的文件。

单击左侧列中找到的“创建用户帐户”链接。 这会将你带到 。~/Membership/CreatingUserAccounts.aspx Web.config由于文件夹中的文件Membership定义了禁止匿名访问的授权规则,中止UrlAuthorizationModule请求并返回 HTTP 401 未授权状态。 这 FormsAuthenticationModule 会将此项修改为 302 重定向状态,并将我们发送到登录页。 请注意,我们尝试访问的页面(CreatingUserAccounts.aspx)通过 ReturnUrl querystring 参数传递到登录页。

由于 URL 授权规则禁止匿名访问,我们将重定向到登录页

图 5:由于 URL 授权规则禁止匿名访问,我们将重定向到登录页(单击以查看全尺寸图像

成功登录后,我们将重定向到 CreatingUserAccounts.aspx 页面。 这一次允许 UrlAuthorizationModule 访问页面,因为我们不再匿名。

将 URL 授权规则应用于特定位置

<system.web>Web.config本节中定义的授权设置适用于该目录及其子目录中的所有 ASP.NET 资源(除非由另一个Web.config文件替代)。 但在某些情况下,我们可能希望给定目录中的所有 ASP.NET 资源都具有特定的授权配置,但一两个特定页面除外。 为此,可以添加一个 <location> 元素 Web.config,将其指向其授权规则不同的文件,并在此处定义其唯一的授权规则。

为了说明如何使用 <location> 元素替代特定资源的配置设置,让我们自定义授权设置,以便只有 Tito 才能访问 CreatingUserAccounts.aspx。 为此,请将元素 <location> 添加到 Membership 文件夹 Web.config 的文件并更新其标记,使其如下所示:

<?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>

中的 <authorization> 元素 <system.web> 定义文件夹及其子文件夹中 ASP.NET 资源 Membership 的默认 URL 授权规则。 该 <location> 元素允许我们替代特定资源的这些规则。 在上面的标记中, <location> 元素引用 CreatingUserAccounts.aspx 页面并指定其授权规则,例如允许 Tito,但拒绝其他人。

若要测试此授权更改,请首先以匿名用户身份访问网站。 如果尝试访问文件夹中的任何页面 Membership ,例如 UserBasedAuthorization.aspxUrlAuthorizationModule 将拒绝请求,并且会重定向到登录页。 例如,在登录后,斯科特可以访问文件夹中的任何页面Membership,但除外CreatingUserAccounts.aspx 尝试以任何人身份访问 CreatingUserAccounts.aspx 登录,但 Tito 将导致未经授权的访问尝试,将你重定向回登录页。

注意

<location> 元素必须出现在配置 <system.web> 元素的外部。 需要为每个要重写其授权设置的资源使用单独的 <location> 元素。

查看UrlAuthorizationModule如何使用授权规则授予或拒绝访问

确定 UrlAuthorizationModule 是否通过分析 URL 授权规则一次一个来授权特定 URL 的特定标识,从第一个 URL 开始并运行其方式。 找到匹配项后,用户就会获得或拒绝访问权限,具体取决于匹配项是在某个 <allow><deny> 元素中找到的。 如果未找到匹配项,则授予用户访问权限。 因此,如果要限制访问,则必须使用元素作为 URL 授权配置中的最后一个 <deny> 元素。 如果省略某个<deny>元素,将授予所有用户访问权限。

若要更好地了解用于确定颁发机构的过程 UrlAuthorizationModule ,请考虑此步骤前面介绍的示例 URL 授权规则。 第一个规则是允许 <allow> 访问 Tito 和 Scott 的元素。 第二个 <deny> 规则是拒绝访问每个人的元素。 如果匿名用户访问,首先 UrlAuthorizationModule 询问,匿名是斯科特还是蒂托? 显然,答案是“否”,因此它继续执行第二条规则。 每个人都在一组匿名吗? 由于此处的答案为“是”,因此规则 <deny> 生效,访问者将重定向到登录页。 同样,如果吉森正在访问, UrlAuthorizationModule 首先问,吉松是斯科特还是蒂托? 既然她不是, UrlAuthorizationModule 第二个问题,吉森在一套每个人都吗? 她也是,所以她也被拒绝访问。 最后,如果蒂托访问,提出的第一个问题 UrlAuthorizationModule 是肯定的答案,所以蒂托获得访问权限。

由于从 UrlAuthorizationModule 上到下处理授权规则,因此在任何匹配时停止,因此必须让更具体的规则位于不太具体的规则之前。 也就是说,要定义禁止 Jisun 和匿名用户的授权规则,但允许所有其他经过身份验证的用户,你将从影响 Jisun 的最具体规则(影响 Jisun 的规则)开始,然后继续执行不太具体的规则-这些规则允许所有其他经过身份验证的用户,但拒绝所有匿名用户。 以下 URL 授权规则首先拒绝 Jisun,然后拒绝任何匿名用户来实现此策略。 除 Jisun 以外的任何经过身份验证的用户都将被授予访问权限,因为这些语句都不匹配 <deny>

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

步骤 2:修复未经授权的经过身份验证的用户的工作流

如本教程前面在“查看 URL 授权工作流”部分中所述,每当未经授权的请求发生时,中止 UrlAuthorizationModule 请求并返回 HTTP 401 未授权状态。 此 401 状态由 FormsAuthenticationModule 302 重定向状态修改,该状态将用户发送到登录页。 此工作流发生在任何未经授权的请求上,即使用户已经过身份验证也是如此。

将经过身份验证的用户返回到登录页可能会让他们感到困惑,因为他们已经登录到系统。 通过稍做一些工作,我们可以通过将经过身份验证的用户重定向到说明他们尝试访问受限页面的页面来改进此工作流。

首先,在名为 UnauthorizedAccess.aspxWeb 应用程序的根文件夹中创建新的 ASP.NET 页;不要忘记将此页与 Site.master 母版页相关联。 创建此页面后,删除引用 LoginContent ContentPlaceHolder 的内容控件,以便显示母版页的默认内容。 接下来,添加一条消息,说明这种情况,即用户尝试访问受保护资源。 添加此类消息后, 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 逻辑被埋在类的私有方法中,因此我们不能自定义此行为。 但是,我们可以执行的操作是将自己的逻辑添加到将用户重定向到 UnauthorizedAccess.aspx的登录页(如果需要)。

FormsAuthenticationModule 将未经授权的访问者重定向到登录页时,它将请求的、未经授权的 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 页面。 若要查看此逻辑,请以匿名访问者身份访问该网站,然后单击左侧列中的“创建用户帐户”链接。 这会将你带到 ~/Membership/CreatingUserAccounts.aspx 页面,在步骤 1 中,我们配置为仅允许访问 Tito。 由于禁止匿名用户,我们 FormsAuthenticationModule 重定向回登录页。

此时,我们是匿名的,所以 Request.IsAuthenticated 返回, false 我们不会重定向到 UnauthorizedAccess.aspx。 而是显示登录页。 以 Tito 以外的用户身份登录,例如 Bruce。 输入相应的凭据后,登录页会将我们重定向回 。~/Membership/CreatingUserAccounts.aspx 但是,由于此页仅可供 Tito 访问,因此我们未经授权查看它,并立即返回到登录页。 但是 Request.IsAuthenticated ,这次返回 true (和 ReturnUrl querystring 参数存在),因此我们将重定向到 UnauthorizedAccess.aspx 页面。

经过身份验证,未经授权的用户将被重定向到UnauthorizedAccess.aspx

图 6:经过身份验证、未经授权的用户将被重定向到 UnauthorizedAccess.aspx单击以查看完整尺寸的图像

通过缩短图 2 中所述的周期,此自定义工作流提供更明智的简单用户体验。

步骤 3:根据当前登录的用户限制功能

使用 URL 授权可以轻松指定粗略的授权规则。 正如我们在步骤 1 中看到的,使用 URL 授权,我们可以简洁地说明允许哪些标识,以及哪些标识被拒绝查看特定页面或文件夹中的所有页面。 但是,在某些情况下,我们可能希望允许所有用户访问某个页面,但基于访问页面的用户限制页面的功能。

请考虑电子商务网站的情况,该网站允许经过身份验证的访问者查看其产品。 当匿名用户访问产品的页面时,他们将只看到产品信息,并且不会有机会离开评审。 但是,访问同一页的经过身份验证的用户将看到审阅界面。 如果经过身份验证的用户尚未查看此产品,界面将允许他们提交评审;否则,它将向他们显示他们以前提交的审查。 若要进一步执行此方案,产品页面可能会显示其他信息,并为那些为电子商务公司工作的用户提供扩展功能。 例如,产品页面可能会列出库存,并在员工访问时包含用于编辑产品价格和说明的选项。

此类细粒度授权规则可以以声明方式或编程方式实现(或通过两者的某种组合实现)。 在下一部分中,我们将了解如何通过 LoginView 控件实现精细授权。 接下来,我们将探索编程技术。 但是,在查看应用精细授权规则之前,我们首先需要创建一个页面,其功能取决于访问它的用户。

让我们创建一个页面,其中列出了 GridView 中特定目录中的文件。 除了列出每个文件的名称、大小和其他信息外,GridView 还将包含 LinkButtons 的两列:一个标题为“视图”,另一列标题为“删除”。 如果单击“查看 LinkButton”,将显示所选文件的内容;如果单击“删除 LinkButton”,则会删除该文件。 我们首先创建此页面,以便其视图和删除功能可供所有用户使用。 在“使用 LoginView 控件和以编程方式限制功能”部分中,我们将了解如何根据访问页面的用户启用或禁用这些功能。

注意

我们即将生成的 ASP.NET 页使用 GridView 控件显示文件列表。 由于本教程系列重点介绍表单身份验证、授权、用户帐户和角色,因此我不想花太多时间讨论 GridView 控件的内部工作。 虽然本教程提供了有关设置此页面的特定分步说明,但它不会深入探讨为何做出某些选择或特定属性对呈现的输出产生什么影响的详细信息。 有关 GridView 控件的彻底检查,请参阅我在 ASP.NET 2.0 教程系列中的“处理数据”。

首先打开Membership文件夹中的文件,UserBasedAuthorization.aspx并将 GridView 控件添加到名为FilesGrid的页面。 在 GridView 的智能标记中,单击“编辑列”链接以启动“字段”对话框。 在此处取消选中左下角的“自动生成字段”复选框。 接下来,添加一个“选择”按钮、一个“删除”按钮和左上角的两个 BoundField(在 CommandField 类型下可以找到“选择和删除”按钮)。 将“选择”按钮 SelectText 的属性设置为“查看”,并将第一个 BoundField 的属性 HeaderTextDataField 属性设置为“名称”。 将 HeaderText 第二个 BoundField 的属性设置为 Size in Bytes,其 DataField 属性设置为 Length,其 DataFormatString 属性设置为 {0:N0} HtmlEncode False。

配置 GridView 的列后,单击“确定”关闭“字段”对话框。 在属性窗口中,将 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 的标记后,我们便可以编写代码,该代码将检索特定目录中的文件并将其绑定到 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 获取应用程序根文件夹中的文件列表。 该方法GetFiles()将目录中的所有文件作为对象数组FileInfo返回,然后绑定到 GridView。 该FileInfo对象具有各种属性,例如NameLength,等等IsReadOnly。 从声明性标记中可以看到,GridView 仅 Name 显示和 Length 属性。

注意

命名空间DirectoryInfoSystem.IO可以找到这些和FileInfo类。 因此,需要用其命名空间名称来设置这些类名的前面,或者将命名空间导入到类文件中(通过 using System.IO)。

请花点时间通过浏览器访问此页面。 它将显示驻留在应用程序的根目录中的文件列表。 单击任何视图或删除 LinkButton 将导致回发,但不会发生任何操作,因为我们尚未创建必要的事件处理程序。

GridView 列出 Web 应用程序的根目录中的文件

图 7:GridView 列出 Web 应用程序的根目录中的文件(单击可查看全尺寸图像

我们需要一种方法来显示所选文件的内容。 返回到 Visual Studio 并添加一 FileContents 个名为 GridView 的 TextBox。 将其 TextMode 属性 MultiLine Columns 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 用于将所选文件的内容读入字符串,然后分配给 FileContents TextBox Text 的属性,从而显示页面上所选文件的内容。

所选文件的内容显示在 TextBox 中

图 8:所选文件的内容显示在 TextBox 中(单击以查看全尺寸图像

注意

如果查看包含 HTML 标记的文件的内容,然后尝试查看或删除文件,将收到错误 HttpRequestValidationException 。 之所以发生这种情况,是因为在回发时,TextBox 的内容将发回 Web 服务器。 默认情况下,每当检测到潜在的危险回发内容(如 HTML 标记)时,ASP.NET 就会引发 HttpRequestValidationException 错误。 若要禁用此错误发生,请通过添加到ValidateRequest="false"@Page指令关闭页面的请求验证。 有关请求验证的优点以及禁用请求时应采取哪些预防措施的详细信息,请阅读 请求验证 - 防止脚本攻击

最后,为 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);
}

代码仅显示要删除的 TextBox FileContents要删除的文件的完整名称,而不会实际删除该文件。

单击“删除”按钮实际上不会删除文件

图 9:单击“删除”按钮实际上不会删除文件(单击以查看全尺寸图像

在步骤 1 中,我们配置了 URL 授权规则,以禁止匿名用户查看文件夹中的页面 Membership 。 为了更好地展示精细身份验证,让我们允许匿名用户访问页面 UserBasedAuthorization.aspx ,但功能有限。 若要打开此页面供所有用户访问,请将以下 <location> 元素添加到 Web.config 文件夹中的文件 Membership

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

添加此 <location> 元素后,通过注销站点来测试新的 URL 授权规则。 作为匿名用户,应允许你访问该 UserBasedAuthorization.aspx 页面。

目前,任何经过身份验证或匿名的用户都可以访问 UserBasedAuthorization.aspx 页面并查看或删除文件。 让我们让它只有经过身份验证的用户才能查看文件的内容,只有 Tito 才能删除文件。 这种精细的授权规则可以通过声明方式、编程方式或通过这两种方法的组合应用。 让我们使用声明性方法来限制谁可以查看文件的内容;我们将使用编程方法来限制谁可以删除文件。

使用 LoginView 控件

正如我们在以往教程中看到的那样,LoginView 控件可用于显示经过身份验证和匿名用户的不同接口,并提供一种简单的方法来隐藏匿名用户无法访问的功能。 由于匿名用户无法查看或删除文件,因此在经过身份验证的用户访问页面时,我们只需要显示 FileContents TextBox。 为此,请将 LoginView 控件添加到页面,将其命名 LoginViewForFileContentsTextBox,并将 TextBox 的声明性标记移动到 FileContents 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 模板中的 Web 控件不再可从代码隐藏类直接访问。 例如,FilesGridGridView 的SelectedIndexChangedRowDeleting事件处理程序当前使用如下代码引用 FileContents TextBox 控件:

FileContents.Text = text;

但是,此代码不再有效。 无法直接访问 TextBox 中的 FileContents TextBox LoggedInTemplate 。 相反,我们必须使用 FindControl("controlId") 该方法以编程方式引用控件。 更新 FilesGrid 事件处理程序以引用 TextBox,如下所示:

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

将 TextBox 移动到 LoginView 并 LoggedInTemplate 更新页面的代码以使用 FindControl("controlId") 模式引用 TextBox 后,以匿名用户身份访问该页面。 如图 10 所示, FileContents 未显示 TextBox。 但是,仍显示 View LinkButton。

LoginView 控件仅呈现经过身份验证的用户的 FileContents TextBox

图 10:LoginView 控件仅呈现 FileContents 经过身份验证的用户的 TextBox(单击可查看全尺寸图像

隐藏匿名用户的“视图”按钮的一种方法是将 GridView 字段转换为 TemplateField。 这将生成一个模板,其中包含 View LinkButton 的声明性标记。 然后,我们可以将 LoginView 控件添加到 TemplateField,并将 LinkButton 放置在 LoginView 的 LoggedInTemplate控件中,从而隐藏匿名访问者的“视图”按钮。 为此,请单击 GridView 智能标记中的“编辑列”链接以启动“字段”对话框。 接下来,从左下角的列表中选择“选择”按钮,然后单击“将此字段转换为 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>

此时,我们可以将 LoginView 添加到 TemplateField。 以下标记仅显示经过身份验证的用户的 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 处于隐藏状态。 我们将在下一部分中了解如何隐藏整个 GridView 列(而不仅仅是 LinkButton)。

LoginView 控件隐藏匿名访问者的视图 LinkButtons

图 11:LoginView 控件隐藏匿名访问者的视图 LinkButtons (单击可查看全尺寸图像

以编程方式限制功能

在某些情况下,声明性技术不足以将功能限制为页面。 例如,某些页面功能的可用性可能取决于访问页面的用户是匿名还是经过身份验证的条件。 在这种情况下,可以通过编程方式显示或隐藏各种用户界面元素。

为了以编程方式限制功能,我们需要执行两个任务:

  1. 确定访问页面的用户是否可以访问该功能,以及
  2. 根据用户是否有权访问有问题的功能,以编程方式修改用户界面。

为了演示这两个任务的应用程序,我们仅允许 Tito 从 GridView 中删除文件。 然后,我们的第一个任务是确定是 Tito 访问页面。 确定后,我们需要隐藏 GridView 的 Delete 列(或显示)。 GridView 的列可通过其Columns属性访问;仅当其属性设置为 true (默认值)时Visible,才会呈现该列。

将数据绑定到 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;

如我们在“表单身份验证概述”教程中所述User.Identity.Name返回标识的名称。 这对应于登录控件中输入的用户名。 如果它是访问页面的 Tito,则 GridView 的第 Visible 二列的属性设置为 true;否则,它设置为 false。 净结果是,当 Tito 以外的其他人访问该页面(另一个经过身份验证的用户或匿名用户)时,不会呈现 Delete 列(请参阅图 12):但是,当 Tito 访问页面时,“删除”列存在(请参阅图 13)。

当 Tito 以外的人访问时,不会呈现“删除列”(如 Bruce)

图 12:Tito 以外的人访问时不呈现“删除列”(如 Bruce)(单击以查看全尺寸图像

为 Tito 呈现“删除列”

图 13:为 Tito 呈现“删除列”(单击以查看全尺寸图像

步骤 4:将授权规则应用于类和方法

在步骤 3 中,我们禁止匿名用户查看文件的内容,并禁止除 Tito 删除文件外的所有用户。 这是通过声明性和编程技术隐藏未经授权的访问者的关联用户界面元素来实现的。 对于我们的简单示例,正确隐藏用户界面元素非常简单,但更复杂的网站可能有多种不同的方式来执行相同的功能呢? 在将该功能限制为未经授权的用户时,如果忘记隐藏或禁用所有适用的用户界面元素,会发生什么情况?

确保未经授权的用户无法访问特定功能片段的一种简单方法是使用 PrincipalPermission 特性修饰该类或方法。 当 .NET 运行时使用类或执行其中一种方法时,它会检查以确保当前安全上下文有权使用该类或执行该方法。 该 PrincipalPermission 属性提供一种机制,通过该机制可以定义这些规则。

让我们演示如何使用 PrincipalPermission GridView SelectedIndexChangedRowDeleting 事件处理程序上的属性来分别禁止匿名用户和 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 运行时将引发一个 SecurityException

如果安全上下文无权执行该方法,则会引发 SecurityException

图 14:如果安全上下文无权执行该方法,则会引发 a SecurityException单击以查看全尺寸图像

注意

若要允许多个安全上下文访问类或方法,请使用每个安全上下文的属性修饰类或方法 PrincipalPermission 。 也就是说,若要允许 Tito 和 Bruce 执行RowDeleting事件处理程序,请添加两个PrincipalPermission属性:

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

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

除了 ASP.NET 页,许多应用程序还具有包括各种层的体系结构,例如业务逻辑和数据访问层。 这些层通常作为类库实现,并提供用于执行业务逻辑和数据相关功能的类和方法。 此属性 PrincipalPermission 可用于将这些层应用授权规则。

有关使用 PrincipalPermission 属性定义类和方法的授权规则的详细信息,请参阅 Scott Guthrie 的博客文章 :使用 PrincipalPermissionAttributes将授权规则添加到业务和数据层。

总结

本教程介绍了如何应用基于用户的授权规则。 我们从 ASP 开始。NET 的 URL 授权框架。 在每个请求中,ASP.NET 引擎 UrlAuthorizationModule 会检查应用程序配置中定义的 URL 授权规则,以确定标识是否有权访问请求的资源。 简言之,URL 授权可以轻松地为特定页面或特定目录中的所有页面指定授权规则。

URL 授权框架逐页应用授权规则。 通过 URL 授权,请求标识有权访问特定资源。 但是,许多方案要求更精细的授权规则。 我们可能需要让每个人访问页面,而不是定义允许访问页面的人员,而是根据访问页面的用户显示不同的数据或提供不同的功能。 页面级授权通常涉及隐藏特定的用户界面元素,以防止未经授权的用户访问禁止的功能。 此外,还可以使用属性来限制对类的访问,并针对特定用户执行其方法。

快乐编程!

深入阅读

有关本教程中讨论的主题的详细信息,请参阅以下资源:

关于作者

斯科特·米切尔是多个 ASP/ASP.NET 书籍的作者,4GuysFromRolla.com 的创始人,自1998年以来一直在与Microsoft Web 技术合作。 斯科特担任独立顾问、教练和作家。 他的最新书是 山姆斯在24小时内 ASP.NET 2.0。 斯科特可以在他的博客上 mitchell@4guysfromrolla.com 或通过他的博客联系 http://ScottOnWriting.NET

特别感谢

本教程系列由许多有用的审阅者审阅。 有兴趣查看即将发布的 MSDN 文章? 如果是这样,请把我扔一条线。mitchell@4GuysFromRolla.com