基于角色的授权 (VB)

作者 :Scott Mitchell

注意

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

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

下载代码下载 PDF

本教程首先了解角色框架如何将用户的角色与其安全上下文相关联。 然后,它检查如何应用基于角色的 URL 授权规则。 接下来,我们将了解如何使用声明性和编程方式来更改所显示的数据以及 ASP.NET 页面提供的功能。

简介

基于用户的授权 教程中,我们了解了如何使用 URL 授权来指定用户可以访问特定页面集的内容。 只需在 中 Web.config添加一点标记,就可以指示 ASP.NET 仅允许经过身份验证的用户访问页面。 或者,我们可以规定仅允许 Tito 和 Bob 用户,或者指示允许除 Sam 之外的所有经过身份验证的用户。

除了 URL 授权之外,我们还了解了声明性和编程技术,用于控制显示的数据以及基于用户访问的页面提供的功能。 具体而言,我们创建了一个页面,其中列出了当前目录的内容。 任何人都可以访问此页面,但只有经过身份验证的用户才能查看文件的内容,只有 Tito 可以删除这些文件。

逐个用户应用授权规则可能会发展成记账噩梦。 更易维护的方法是使用基于角色的授权。 好消息是,我们掌握的用于应用授权规则的工具与角色一样适用于用户帐户。 URL 授权规则可以指定角色而不是用户。 LoginView 控件(可为经过身份验证的用户和匿名用户呈现不同的输出)可以配置为根据登录用户的角色显示不同的内容。 角色 API 包括用于确定已登录用户角色的方法。

本教程首先了解角色框架如何将用户的角色与其安全上下文相关联。 然后,它检查如何应用基于角色的 URL 授权规则。 接下来,我们将了解如何使用声明性和编程方式来更改所显示的数据以及 ASP.NET 页面提供的功能。 现在就开始吧!

了解角色如何与用户的安全上下文相关联

每当请求进入 ASP.NET 管道时,它都与安全上下文相关联,其中包括标识请求者的信息。 使用 Forms 身份验证时,身份验证票证用作标识令牌。 正如我们在表单身份验证概述教程中所述FormsAuthenticationModule 负责确定请求者的标识,它在事件期间AuthenticateRequest会这样做。

如果找到有效的、未过期的身份验证票证,则会 FormsAuthenticationModule 对其进行解码以确定请求者的身份。 它创建一个新的 GenericPrincipal 对象,并将其分配给 该 HttpContext.User 对象。 主体(如 GenericPrincipal)的用途是标识经过身份验证的用户的名称以及她所属的角色。 所有主体对象都有一个属性和一个 Identity 方法,这一 IsInRole(roleName) 目的就很明显了。 FormsAuthenticationModule但是, 对记录角色信息不感兴趣,GenericPrincipal并且它创建的对象未指定任何角色。

如果启用了角色框架,HTTP 模块会在 RoleManagerModule 事件后执行FormsAuthenticationModule,并标识事件期间PostAuthenticateRequest经过身份验证的用户的角色,该角色在AuthenticateRequest事件后触发。 如果请求来自经过身份验证的用户, RoleManagerModuleGenericPrincipal覆盖由 FormsAuthenticationModule 创建的 对象,并将其替换为 对象RolePrincipal。 类 RolePrincipal 使用角色 API 来确定用户所属的角色。

图 1 描述了使用表单身份验证和角色框架时的 ASP.NET 管道工作流。 FormsAuthenticationModule首先执行,通过身份验证票证标识用户,并创建一个新的 GenericPrincipal 对象。 接下来,和 RoleManagerModule 中的步骤使用 GenericPrincipalRolePrincipal 对象覆盖 对象。

如果匿名用户访问站点,则 和 RoleManagerModule 都不会FormsAuthenticationModule创建主体对象。

使用表单身份验证和角色框架时经过身份验证的用户的 ASP.NET 管道事件

图 1:使用表单身份验证时经过身份验证的用户的 ASP.NET 管道事件和角色框架 (单击以查看全尺寸图像)

对象的 RolePrincipalIsInRole(roleName) 方法调用 RolesGetRolesForUser 获取用户的角色,以确定用户是否为 roleName 的成员。 使用 时, SqlRoleProvider这会导致对角色存储数据库进行查询。 使用基于角色的 URL 授权规则时, RolePrincipal将对受基于角色的 IsInRole URL 授权规则保护的页面的每个请求调用 的 方法。 框架包含用于在 Cookie 中缓存用户角色的选项, Roles 而不必在每个请求中查找数据库中的角色信息。

如果将角色框架配置为在 Cookie 中缓存用户的角色,则会 RoleManagerModule 在 ASP.NET 管道 EndRequest 事件期间创建 Cookie。 此 Cookie 用于 中的 PostAuthenticateRequest后续请求,即创建 对象时 RolePrincipal 。 如果 Cookie 有效且尚未过期,则会分析 Cookie 中的数据并用于填充用户的角色,从而不必 RolePrincipal 调用 Roles 类来确定用户的角色。 图 2 描绘了此工作流。

用户的角色信息可以存储在 Cookie 中以提高性能

图 2:可将用户的角色信息存储在 Cookie 中以提高性能 (单击以查看全尺寸图像)

默认情况下,角色缓存 Cookie 机制处于禁用状态。 可以通过 中的 Web.config配置标记来启用<roleManager>它。 我们在创建和管理角色教程中讨论了如何使用 <roleManager> 元素指定角色提供程序,因此应用程序文件中应已包含此元素Web.config。 角色缓存 Cookie 设置指定为 ; 元素的属性 <roleManager>,表 1 中汇总了这些设置。

注意

表 1 中列出的配置设置指定生成的角色缓存 Cookie 的属性。 有关 Cookie、其工作原理及其各种属性的详细信息,请阅读 此 Cookie 教程

属性 说明
cacheRolesInCookie 一个布尔值,指示是否使用 Cookie 缓存。 默认为 false
cookieName 角色缓存 Cookie 的名称。 默认值为 “ 。ASPXROLES”。
cookiePath 角色名称 Cookie 的路径。 path 属性使开发人员能够将 Cookie 的范围限制为特定的目录层次结构。 默认值为“/”,通知浏览器将身份验证票证 Cookie 发送到对域发出的任何请求。
cookieProtection 指示使用哪些技术来保护角色缓存 Cookie。 允许的值为: All (默认) ; Encryption; NoneValidation.md)

| cookieRequireSSL |一个布尔值,指示传输身份验证 Cookie 是否需要 SSL 连接。 默认值为 false cookieSlidingExpiration false createPersistentCookieis set totrue. | | cookieTimeout | Specifies the time, in minutes, after which the authentication ticket cookie expires. The default value is30. This value is only pertinent when createPersistentCookieis set totrue. | | createPersistentCookie | A Boolean value that specifies whether the role cache cookie is a session cookie or persistent cookie. Iffalse(the default), a session cookie is used, which is deleted when the browser is closed. Iftrue, a persistent cookie is used; it expires cookieTimeoutnumber of minutes after it has been created or after the previous visit, depending on the value ofcookieSlidingExpiration. | | domain| Specifies the cookie's domain value. The default value is an empty string, which causes the browser to use the domain from which it was issued (such as www.yourdomain.com). In this case, the cookie will <strong>not</strong> be sent when making requests to subdomains, such as admin.yourdomain.com. If you want the cookie to be passed to all subdomains you need to customize thedomain maxCachedResults | Specifies the maximum number of role names that are cached in the cookie. The default is 25. Theattribute, setting it to "yourdomain.com". | | RoleManagerModuledoes not create a cookie for users that belong to more thanmaxCachedResultsroles. Consequently, theRolePrincipalobject'sIsInRolemethod will use theRolesclass to determine the user's roles. The reasonmaxCachedResultsexists is because many user agents do not permit cookies larger than 4,096 bytes. So this cap is meant to reduce the likelihood of exceeding this size limitation. If you have extremely long role names, you may want to consider specifying a smaller. This value is only pertinent when | A Boolean value that indicates whether the cookie's timeout is reset each time the user visits the site during a single session. The default value is. | | maxCachedResults 的值;相反,如果角色名称非常短,则可能会增加此值。 |

表 1:角色缓存 Cookie 配置选项

让我们将应用程序配置为使用非持久性角色缓存 Cookie。 为此,请更新 <roleManager> 中的 Web.config 元素,以包含以下与 Cookie 相关的属性:

<roleManager enabled="true" 
          defaultProvider="SecurityTutorialsSqlRoleProvider"
          cacheRolesInCookie="true"
          createPersistentCookie="false"
          cookieProtection="All">

     <providers>
     ...
     </providers>
</roleManager>

我通过添加三个属性来更新 <roleManager>; 元素: cacheRolesInCookiecreatePersistentCookiecookieProtection。 通过将 设置为 cacheRolesInCookietrueRoleManagerModule 现在 将自动将用户的角色缓存在 Cookie 中,而不必在每个请求中查找用户的角色信息。 我分别将 createPersistentCookiecookieProtection 属性显式设置为 falseAll。 从技术上讲,我不需要为这些属性指定值,因为我刚刚将它们分配给了默认值,但我将它们放在此处是为了明确表明我未使用持久性 Cookie,并且 Cookie 已加密和验证。

就是这么简单! 此后,角色框架将在 Cookie 中缓存用户的角色。 如果用户的浏览器不支持 Cookie,或者他们的 Cookie 被删除或丢失,则这没什么大不了的 - RolePrincipal 对象将只使用 Roles 类,以防没有 cookie (或) 无效或过期。

注意

Microsoft 的模式 & 做法组不建议使用永久性角色缓存 Cookie。 由于拥有角色缓存 Cookie 足以证明角色成员身份,如果黑客以某种方式获得有效用户 Cookie 的访问权限,他就可以模拟该用户。 如果 Cookie 保留在用户的浏览器中,则发生这种情况的可能性会增加。 有关此安全建议以及其他安全问题的详细信息,请参阅 ASP.NET 2.0 的安全问题列表

步骤 1:定义 Role-Based URL 授权规则

基于用户的授权教程中所述,URL 授权提供了一种在用户或角色的基础上限制对一组页面的访问的方法。 URL 授权规则在 中使用 Web.config<authorization> 元素<allow><deny>子元素进行拼写。 除了前面教程中讨论的用户相关授权规则外,每个 <allow> 元素和 <deny> 子元素还可以包括:

  • 特定角色
  • 以逗号分隔的角色列表

例如,URL 授权规则向具有管理员和监督者角色的用户授予访问权限,但拒绝所有其他用户的访问权限:

<authorization>

     <allow roles="Administrators, Supervisors" />
     <deny users="*" />
</authorization>

<allow>上述标记中的 元素声明允许管理员和监督者角色;<deny>; 元素指示拒绝所有用户

让我们配置应用程序, ManageRoles.aspx以便只有具有管理员角色的用户才能访问 、 UsersAndRoles.aspxCreateUserWizardWithRoles.aspx 页面,而 RoleBasedAuthorization.aspx 页面仍可供所有访问者访问。

为此,请首先将文件添加到 Web.configRoles 文件夹。

将 Web.config 文件添加到 Roles 目录

图 3:将文件添加到Web.configRoles目录 (单击以查看全尺寸图像)

接下来,将以下配置标记添加到 Web.config

<?xml version="1.0"?>

<configuration>
     <system.web>
          <authorization>
               <allow roles="Administrators" />
               <deny users="*"/>
          </authorization>

     </system.web>

     <!-- Allow all users to visit RoleBasedAuthorization.aspx -->
     <location path="RoleBasedAuthorization.aspx">
          <system.web>
               <authorization>
                    <allow users="*" />

               </authorization>
          </system.web>
     </location>
</configuration>

<authorization>部分中的 <system.web> 元素指示只有管理员角色的用户才能访问目录中的 ASP.NET 资源Roles。 元素 <location> 为页面定义一组备用的 URL 授权规则 RoleBasedAuthorization.aspx ,以允许所有用户访问页面。

保存对 Web.config的更改后,以非管理员角色的用户身份登录,然后尝试访问其中一个受保护的页面。 UrlAuthorizationModule将检测到你无权访问请求的资源;因此, FormsAuthenticationModule 会将你重定向到登录页。 然后,登录页会将你重定向到 UnauthorizedAccess.aspx 页面, (见图 4) 。 由于我们在基于用户的授权教程的步骤 2 中添加到登录页的代码,因此从登录UnauthorizedAccess.aspx页到 的最终重定向发生。 具体而言,如果 querystring 包含参数ReturnUrl,登录页会自动将任何经过身份验证的用户UnauthorizedAccess.aspx重定向到 ,因为此参数指示用户在尝试查看他无权查看的页面后到达登录页。

只有管理员角色中的用户才能查看受保护的页面

图 4:只有具有管理员角色的用户才能查看受保护的页面 (单击以查看全尺寸图像)

注销,然后以管理员角色的用户身份登录。 现在,你应该能够查看三个受保护的页面。

Tito 可以访问 UsersAndRoles.aspx 页面,因为他是管理员角色

图 5:Tito 可以访问页面, UsersAndRoles.aspx 因为他处于管理员角色 (单击以查看全尺寸图像)

注意

为角色或用户指定 URL 授权规则时,请务必记住,从上到下逐个分析规则。 一旦找到匹配项,将立即授予或拒绝用户访问权限,具体取决于是否在 或 <deny> 元素中找到<allow>匹配项。 如果未找到匹配项,则向用户授予访问权限。 因此,如果要限制对一个或多个用户帐户的访问,则必须使用 元素作为 URL 授权配置中的最后一个 <deny> 元素。 如果 URL 授权规则不包含<deny>元素,将授予所有用户访问权限。有关如何分析 URL 授权规则的更全面讨论,请参阅基于用户的授权教程的“如何使用UrlAuthorizationModule授权规则授予或拒绝访问”部分

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

通过 URL 授权,可以轻松指定粗略的授权规则,这些规则规定允许哪些标识以及哪些标识无法查看特定页面 (或文件夹及其子文件夹中的所有页面) 。 但是,在某些情况下,我们可能希望允许所有用户访问某个页面,但根据访问用户的角色限制页面的功能。 这可能需要根据用户的角色显示或隐藏数据,或者为属于特定角色的用户提供其他功能。

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

让我们创建一个页面,在 GridView 中列出系统中的所有用户帐户。 GridView 将包括每个用户的用户名、电子邮件地址、上次登录日期和有关用户的评论。 除了显示每个用户的信息外,GridView 还将包括编辑和删除功能。 我们最初将使用所有用户可用的编辑和删除功能创建此页面。 在“使用 LoginView 控件”和“以编程方式限制功能”部分中,我们将了解如何根据访问用户的角色启用或禁用这些功能。

注意

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

首先打开 RoleBasedAuthorization.aspx 文件夹中的页面 Roles 。 将 GridView 从页面拖到Designer并将其设置为 IDUserGrid。 稍后,我们将编写调用 的代码 MembershipGetAllUsers 方法 并将生成的 MembershipUserCollection 对象绑定到 GridView。 包含MembershipUserCollection系统中每个用户帐户的 对象MembershipUser;MembershipUser对象具有 、EmailLastLoginDate等属性UserName

在编写将用户帐户绑定到网格的代码之前,让我们先定义 GridView 的字段。 在 GridView 的智能标记中,单击“编辑列”链接以启动“字段”对话框, (请参阅图 6) 。 在此处取消选中左下角的“自动生成字段”复选框。 由于我们希望此 GridView 包含编辑和删除功能,因此请添加 CommandField 并将其 和 ShowDeleteButton 属性设置为 ShowEditButton True。 接下来,添加用于显示 、、 EmailLastLoginDateComment 属性的UserName四个字段。 将 BoundField 用于两个只读属性 (UserNameLastLoginDate) ,并将 TemplateFields 用于两个可编辑字段 (EmailComment) 。

让第一个 BoundField 显示 UserName 属性;将其 HeaderTextDataField 属性设置为“UserName”。 此字段不可编辑,因此请将其 ReadOnly 属性设置为 True。 通过将 LastLoginDate BoundField 设置为“Last Login”,将其DataField设置为HeaderText“LastLoginDate”来配置 BoundField。 让我们设置此 BoundField 的输出格式,以便只显示 (日期,而不是) 日期和时间。 为此,请将此 BoundField 的 HtmlEncode 属性设置为 False,将其 DataFormatString 属性设置为“{0:d}”。 此外,将 ReadOnly 属性设置为 True。

HeaderText两个 TemplateField 的属性设置为“Email”和“注释”。

可以通过“字段”对话框配置 GridView 的字段

图 6:可以通过“字段”对话框配置 GridView 的字段 (单击以查看全尺寸图像)

我们现在需要为“Email”和EditItemTemplate“注释”TemplateFields 定义 ItemTemplate 和 。 向每个 ItemTemplates 添加标签 Web 控件,并分别将其 Text 属性绑定到 EmailComment 属性。

对于“Email”TemplateField,将名为 Email 的 TextBox 添加到其 EditItemTemplate ,并使用双向数据绑定将其Text属性绑定到 Email 属性。 将 RequiredFieldValidator 和 RegularExpressionValidator 添加到 EditItemTemplate ,以确保编辑 Email 属性的访问者输入了有效的电子邮件地址。 对于“注释”TemplateField,将名为 Comment 的多行 TextBox 添加到其 EditItemTemplate。 将 TextBox 的 ColumnsRows 属性分别设置为 40 和 4,然后使用双向数据绑定将其 Text 属性绑定到 Comment 属性。

配置这些 TemplateFields 后,其声明性标记应如下所示:

<asp:TemplateField HeaderText="Email">
     <ItemTemplate>
          <asp:Label runat="server" ID="Label1" Text='<%# Eval("Email")%>'></asp:Label>

     </ItemTemplate>
     <EditItemTemplate>
          <asp:TextBox runat="server" ID="Email" Text='<%# Bind("Email")%>'></asp:TextBox>

          <asp:RequiredFieldValidator ID="RequiredFieldValidator1" runat="server" 
               ControlToValidate="Email" Display="Dynamic"
               ErrorMessage="You must provide an email address."
               SetFocusOnError="True">*</asp:RequiredFieldValidator>

          <asp:RegularExpressionValidator ID="RegularExpressionValidator1" runat="server"
               ControlToValidate="Email" Display="Dynamic"
               ErrorMessage="The email address you have entered is not valid. Please fix 
               this and try again."
               SetFocusOnError="True"

               ValidationExpression="\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*">*
          </asp:RegularExpressionValidator>
     </EditItemTemplate>
</asp:TemplateField>

<asp:TemplateField HeaderText="Comment">
     <ItemTemplate>
          <asp:Label runat="server" ID="Label2" Text='<%# Eval("Comment")%>'></asp:Label>

     </ItemTemplate>
     <EditItemTemplate>
          <asp:TextBox runat="server" ID="Comment" TextMode="MultiLine"
               Columns="40" Rows="4" Text='<%# Bind("Comment")%>'>

          </asp:TextBox>
     </EditItemTemplate>
</asp:TemplateField>

编辑或删除用户帐户时,我们需要知道该用户的 UserName 属性值。 将 GridView 的 DataKeyNames 属性设置为“UserName”,以便通过 GridView 的 DataKeys 集合获取此信息。

最后,将 ValidationSummary 控件添加到页面,并将其 ShowMessageBox 属性设置为 True,将其 ShowSummary 属性设置为 False。 使用这些设置,如果用户尝试编辑电子邮件地址缺失或无效的用户帐户,ValidationSummary 将显示客户端警报。

<asp:ValidationSummary ID="ValidationSummary1"
               runat="server"
               ShowMessageBox="True"
               ShowSummary="False" />

现已完成此页面的声明性标记。 下一个任务是将用户帐户集绑定到 GridView。 将名为 BindUserGrid 的方法添加到RoleBasedAuthorization.aspx页面的代码隐藏类,该类将 返回的 Membership.GetAllUsers 绑定到 MembershipUserCollectionUserGrid GridView。 在第一次访问页面时, Page_Load 从事件处理程序调用此方法。

Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
     If Not Page.IsPostBack Then
          BindUserGrid()
     End If
End Sub

Private Sub BindUserGrid()
     Dim allUsers As MembershipUserCollection = Membership.GetAllUsers()
     UserGrid.DataSource = allUsers
     UserGrid.DataBind()
End Sub

完成此代码后,通过浏览器访问页面。 如图 7 所示,应会看到一个 GridView,其中列出了有关系统中每个用户帐户的信息。

UserGrid GridView 列出有关系统中每个用户的信息

图 7UserGrid GridView 列出有关系统中每个用户的信息 (单击以查看全尺寸图像)

注意

GridView UserGrid 列出非分页界面中的所有用户。 此简单的网格界面不适用于有几十个或更多用户的情况。 一个选项是配置 GridView 以启用分页。 方法 Membership.GetAllUsers 有两个重载:一个不接受输入参数并返回所有用户,一个重载采用页面索引和页面大小的整数值,仅返回指定的用户子集。 第二个重载可用于更有效地翻页浏览用户,因为它只返回用户帐户的精确子集,而不是 全部 用户帐户。 如果你有数千个用户帐户,则可能需要考虑一个基于筛选器的界面,例如,该界面仅显示其 UserName 以所选字符开头的用户。 方法Membership.FindUsersByName非常适合用于生成基于筛选器的用户界面。 我们将在未来的教程中介绍如何生成此类接口。

当控件绑定到正确配置的数据源控件(如 SqlDataSource 或 ObjectDataSource)时,GridView 控件提供内置的编辑和删除支持。 UserGrid但是,GridView 以编程方式绑定其数据;因此,我们必须编写代码来执行这两项任务。 具体而言,我们需要为 GridView 的 RowEditingRowCancelingEditRowUpdatingRowDeleting 事件创建事件处理程序,这些事件处理程序是在访问者单击 GridView 的“编辑”、“取消”、“更新”或“删除”按钮时触发的。

首先,为 GridView 的 RowEditingRowCancelingEditRowUpdating 事件创建事件处理程序,然后添加以下代码:

Protected Sub UserGrid_RowEditing(ByVal sender As Object, ByVal e As System.Web.UI.WebControls.GridViewEditEventArgs) Handles UserGrid.RowEditing
     ' Set the grid's EditIndex and rebind the data

     UserGrid.EditIndex = e.NewEditIndex
     BindUserGrid()
End Sub

Protected Sub UserGrid_RowCancelingEdit(ByVal sender As Object, ByVal e As System.Web.UI.WebControls.GridViewCancelEditEventArgs) Handles UserGrid.RowCancelingEdit
     ' Revert the grid's EditIndex to -1 and rebind the data
     UserGrid.EditIndex = -1
     BindUserGrid()
End Sub
    
Protected Sub UserGrid_RowUpdating(ByVal sender As Object, ByVal e As System.Web.UI.WebControls.GridViewUpdateEventArgs) Handles UserGrid.RowUpdating
     ' Exit if the page is not valid
     If Not Page.IsValid Then
          Exit Sub
     End If

     ' Determine the username of the user we are editing
     Dim UserName As String = UserGrid.DataKeys(e.RowIndex).Value.ToString()

     ' Read in the entered information and update the user
     Dim EmailTextBox As TextBox = CType(UserGrid.Rows(e.RowIndex).FindControl("Email"),TextBox)
     Dim CommentTextBox As TextBox= CType(UserGrid.Rows(e.RowIndex).FindControl("Comment"),TextBox)

     ' Return information about the user
     Dim UserInfo As MembershipUser = Membership.GetUser(UserName)

     ' Update the User account information
     UserInfo.Email = EmailTextBox.Text.Trim()
     UserInfo.Comment = CommentTextBox.Text.Trim()

     Membership.UpdateUser(UserInfo)

     ' Revert the grid's EditIndex to -1 and rebind the data
     UserGrid.EditIndex = -1
     BindUserGrid()
End Sub

RowEditingRowCancelingEdit 事件处理程序只需设置 GridView 的 EditIndex 属性,然后将用户帐户列表重新绑定到网格。 有趣的事情发生在事件处理程序中 RowUpdating 。 此事件处理程序首先确保数据有效,然后从DataKeys集合中获取UserName已编辑用户帐户的值。 Email然后以编程方式引用两个 TemplateFields 中的 EditItemTemplateComment TextBox。 其 Text 属性包含已编辑的电子邮件地址和注释。

为了通过成员资格 API 更新用户帐户,我们需要首先获取用户的信息,我们通过调用 Membership.GetUser(userName)来获取用户的信息。 然后,使用从编辑界面输入到两个 TextBox 中的值更新返回 MembershipUser 对象的 EmailComment 属性。 最后,通过调用 Membership.UpdateUser保存这些修改。 事件处理程序 RowUpdating 通过将 GridView 还原到其预编辑界面来完成。

接下来,创建 RowDeleting RowDeleting 事件处理程序,然后添加以下代码:

Protected Sub UserGrid_RowDeleting(ByVal sender As Object, ByVal e As System.Web.UI.WebControls.GridViewDeleteEventArgs) Handles UserGrid.RowDeleting

     ' Determine the username of the user we are editing
     Dim UserName As String = UserGrid.DataKeys(e.RowIndex).Value.ToString()

     ' Delete the user
     Membership.DeleteUser(UserName)

     ' Revert the grid's EditIndex to -1 and rebind the data
     UserGrid.EditIndex = -1
     BindUserGrid()
End Sub

上述事件处理程序首先从 GridView 的DataKeys集合中获取UserName值;然后将此值UserName传递到 Membership 类的 DeleteUser 方法中。 方法 DeleteUser 从系统中删除用户帐户,包括相关成员身份数据 (,例如此用户属于哪些角色) 。 删除用户后,网格的 EditIndex 设置为 -1 (以防用户单击“删除”而另一行处于编辑模式) 并 BindUserGrid 调用 方法。

注意

删除用户帐户之前,“删除”按钮不需要用户进行任何形式的确认。 我建议你添加某种形式的用户确认,以减少帐户被意外删除的可能性。 确认操作的最简单方法之一是通过客户端确认对话框。 有关此技术的详细信息,请参阅 删除时添加 Client-Side 确认

验证此页面是否按预期运行。 你应该能够编辑任何用户的电子邮件地址和评论,以及删除任何用户帐户。 RoleBasedAuthorization.aspx由于该页面可供所有用户访问,因此任何用户(甚至是匿名访问者)都可以访问此页面并编辑和删除用户帐户! 让我们更新此页面,以便只有具有监督者和管理员角色的用户才能编辑用户的电子邮件地址和注释,并且只有管理员才能删除用户帐户。

“使用 LoginView 控件”部分介绍如何使用 LoginView 控件来显示特定于用户角色的说明。 如果具有管理员角色的人员访问此页面,我们将显示有关如何编辑和删除用户的说明。 如果具有“监督者”角色的用户到达此页面,我们将显示有关编辑用户的说明。 如果访问者是匿名的,或者不是“监督者”或“管理员”角色,我们将显示一条消息,说明他们无法编辑或删除用户帐户信息。 在“以编程方式限制功能”部分中,我们将编写基于用户角色以编程方式显示或隐藏“编辑”和“删除”按钮的代码。

使用 LoginView 控件

正如我们在以前的教程中看到的那样,LoginView 控件可用于显示经过身份验证的用户和匿名用户的不同接口,但 LoginView 控件也可用于根据用户的角色显示不同的标记。 让我们使用 LoginView 控件根据访问用户的角色显示不同的说明。

首先,在 GridView 上方 UserGrid 添加 LoginView。 如前所述,LoginView 控件具有两个内置模板: AnonymousTemplateLoggedInTemplate。 在这两个模板中输入一条简短消息,告知用户他们无法编辑或删除任何用户信息。

<asp:LoginView ID="LoginView1" runat="server">
     <LoggedInTemplate>
          You are not a member of the Supervisors or Administrators roles. Therefore you
           cannot edit or delete any user information.
     </LoggedInTemplate>
     <AnonymousTemplate>

          You are not logged into the system. Therefore you cannot edit or delete any user
           information.
     </AnonymousTemplate>
</asp:LoginView>

除了 和 LoggedInTemplate之外,AnonymousTemplateLoginView 控件还可以包括 RoleGroups,它们是特定于角色的模板。 每个 RoleGroup 都包含一个属性 , Roles该属性指定 RoleGroup 应用于的角色。 属性 Roles 可以设置为单个角色 ((如“管理员”) )或逗号分隔的角色列表, (如“管理员,监督员”) 。

若要管理 RoleGroup,请单击控件的智能标记中的“编辑 RoleGroups”链接,打开 RoleGroup 集合编辑器。 添加两个新的 RoleGroup。 将第一个 RoleGroup 的 Roles 属性设置为“Administrators”,将第二个属性设置为“Supervisors”。

通过 RoleGroup 集合编辑器管理 LoginView 的 Role-Specific 模板

图 8:通过 RoleGroup 集合编辑器管理 LoginView 的 Role-Specific 模板 (单击以查看全尺寸图像)

单击“确定”关闭 RoleGroup 集合编辑器;这将更新 LoginView 的声明性标记,以包含一个 <RoleGroups> 节,其中包含 <asp:RoleGroup> RoleGroup 集合编辑器中定义的每个 RoleGroup 的子元素。 此外,LoginView 的智能标记中的“视图”下拉列表(最初只 AnonymousTemplate 列出了 和 LoggedInTemplate ),现在还包括添加的 RoleGroups。

编辑 RoleGroups,以便向具有监督者角色的用户显示有关如何编辑用户帐户的说明,同时显示管理员角色的用户的编辑和删除说明。 进行这些更改后,LoginView 的声明性标记应如下所示。

<asp:LoginView ID="LoginView1" runat="server">
     <RoleGroups>
          <asp:RoleGroup Roles="Administrators">

               <ContentTemplate>
                    As an Administrator, you may edit and delete user accounts. 
                    Remember: With great power comes great responsibility!
               </ContentTemplate>
          </asp:RoleGroup>
          <asp:RoleGroup Roles="Supervisors">
               <ContentTemplate>
                    As a Supervisor, you may edit users&#39; Email and Comment information. 
                    Simply click the Edit button, make your changes, and then click Update.
               </ContentTemplate>

          </asp:RoleGroup>
     </RoleGroups>
     <LoggedInTemplate>
          You are not a member of the Supervisors or Administrators roles. 
          Therefore you cannot edit or delete any user information.
     </LoggedInTemplate>
     </AnonymousTemplate>
          You are not logged into the system. 
          Therefore you cannot edit or delete any user information.
     </AnonymousTemplate>
</asp:LoginView>

进行这些更改后,保存页面,然后通过浏览器访问它。 首先以匿名用户身份访问页面。 应会显示消息“你未登录到系统。 因此,无法编辑或删除任何用户信息。”然后,以经过身份验证的用户身份登录,但该用户既不是“监督者”角色,也不是“管理员”角色的用户。 这一次,应会看到消息“你不是监督员或管理员角色的成员。 因此,无法编辑或删除任何用户信息。”

接下来,以“监督者”角色成员的用户身份登录。 这一次应看到“特定于监督者角色”的消息 (见图 9) 。 如果以管理员角色的用户身份登录,应会看到特定于管理员角色的消息 (见图 10) 。

布鲁斯被显示主管 Role-Specific 消息

图 9:Bruce 显示“主管 Role-Specific 消息 (单击以查看全尺寸图像)

向 Tito 显示管理员 Role-Specific 消息

图 10:Tito 显示管理员 Role-Specific 消息 (单击以查看全尺寸图像)

如图 9 和图 10 中的屏幕截图所示,即使应用了多个模板,LoginView 也仅呈现一个模板。 Bruce 和 Tito 都是已登录用户,但 LoginView 仅呈现匹配的 RoleGroup,而不呈现 LoggedInTemplate。 此外,Tito 同时属于管理员和监督者角色,但 LoginView 控件呈现特定于管理员角色的模板,而不是监督者模板。

图 11 说明了 LoginView 控件用于确定要呈现的模板的工作流。 请注意,如果指定了多个 RoleGroup,则 LoginView 模板将呈现匹配 的第一个 RoleGroup。 换句话说,如果将 Supervisors RoleGroup 作为第一个 RoleGroup,将管理员作为第二个角色组,那么当 Tito 访问此页面时,他会看到 Supervisors 消息。

用于确定要呈现的模板的 LoginView 控件工作流

图 11:用于确定要呈现的模板的 LoginView 控件工作流 (单击以查看全尺寸图像)

以编程方式限制功能

虽然 LoginView 控件根据访问页面的用户的角色显示不同的说明,但“编辑”和“取消”按钮仍对所有人可见。 我们需要以编程方式隐藏“编辑”和“删除”按钮,以便匿名访问者和用户既不是“监督者”,也不是“管理员”角色。 我们需要为不是管理员的每个人隐藏“删除”按钮。 为了实现此目的,我们将编写一些代码,以编程方式引用 CommandField 的 Edit 和 Delete LinkButtons,并在必要时将其 Visible 属性设置为 False

以编程方式引用 CommandField 中的控件的最简单方法是先将其转换为模板。 为此,请单击 GridView 的智能标记中的“编辑列”链接,从当前字段列表中选择 CommandField,然后单击“将此字段转换为 TemplateField”链接。 这会将 CommandField 转换为具有 和 EditItemTemplateItemTemplate TemplateField。 包含 ItemTemplate Edit 和 Delete LinkButtons,而 包含 EditItemTemplate Update 和 Cancel LinkButtons。

将 CommandField 转换为 TemplateField

图 12:将 CommandField 转换为 TemplateField (单击以查看全尺寸图像)

更新 中的ItemTemplate“编辑”和“删除链接按钮”,将其ID属性分别设置为 和 DeleteButton的值EditButton

<asp:TemplateField ShowHeader="False">
     <EditItemTemplate>
          <asp:LinkButton ID="LinkButton1" runat="server" CausesValidation="True" 
               CommandName="Update" Text="Update"></asp:LinkButton>

           <asp:LinkButton ID="LinkButton2" runat="server" CausesValidation="False"
               CommandName="Cancel" Text="Cancel"></asp:LinkButton>

     </EditItemTemplate>
     <ItemTemplate>
          <asp:LinkButton ID="EditButton" runat="server" CausesValidation="False" 
               CommandName="Edit" Text="Edit"></asp:LinkButton>

           <asp:LinkButton ID="DeleteButton" runat="server" CausesValidation="False"
               CommandName="Delete" Text="Delete"></asp:LinkButton>

     </ItemTemplate>
</asp:TemplateField>

每当数据绑定到 GridView 时,GridView 都会枚举其 DataSource 属性中的记录并生成相应的 GridViewRow 对象。 创建每个 GridViewRow 对象时,将 RowCreated 触发 事件。 为了隐藏未经授权的用户的“编辑”和“删除”按钮,我们需要为此事件创建事件处理程序,并以编程方式引用“编辑”和“删除链接”按钮,并相应地设置其 Visible 属性。

创建事件处理程序 事件 RowCreated ,然后添加以下代码:

Protected Sub UserGrid_RowCreated(ByVal sender As Object, ByVal e As System.Web.UI.WebControls.GridViewRowEventArgs) Handles UserGrid.RowCreated
     If e.Row.RowType = DataControlRowType.DataRow AndAlso e.Row.RowIndex <> UserGrid.EditIndex Then
          ' Programmatically reference the Edit and Delete LinkButtons
          Dim EditButton As LinkButton = CType(e.Row.FindControl("EditButton"), LinkButton)

          Dim DeleteButton As LinkButton = CType(e.Row.FindControl("DeleteButton"), LinkButton)

          EditButton.Visible = (User.IsInRole("Administrators") OrElse User.IsInRole("Supervisors"))
          DeleteButton.Visible = User.IsInRole("Administrators")
     End If
End Sub

请记住, RowCreated 事件针对 所有 GridView 行触发,包括页眉、页脚、寻呼界面等。 如果我们处理的数据行不是处于编辑模式的数据行,我们只想以编程方式引用“编辑”和“删除”链接按钮 (因为编辑模式下的行具有“更新”和“取消”按钮,而不是“编辑和删除”) 。 此检查由 If 语句处理。

如果我们处理的数据行不是处于编辑模式,则会引用 Edit 和 Delete LinkButton,并且其 Visible 属性是根据对象的 方法返回的布尔值设置 UserIsInRole(roleName) 。 对象 User 引用由 RoleManagerModule创建的主体;因此, IsInRole(roleName) 方法使用角色 API 来确定当前访问者是否属于 roleName

注意

我们可以直接使用 Roles 类,将 对 User.IsInRole(roleName) 的调用替换为对 方法的Roles.IsUserInRole(roleName)调用。 在此示例中,我决定使用主体对象的 IsInRole(roleName) 方法,因为它比直接使用角色 API 更高效。 在本教程的前面部分中,我们配置了角色管理器,以在 Cookie 中缓存用户的角色。 仅当调用主体的 方法时,才会使用此缓存的 IsInRole(roleName) Cookie 数据;直接调用角色 API 始终涉及访问角色存储区。 即使角色未缓存在 Cookie 中,调用主体对象的 IsInRole(roleName) 方法通常更有效,因为在请求期间首次调用它时,它会缓存结果。 另一方面,角色 API 不执行任何缓存。 RowCreated由于事件针对 GridView 中的每一行触发一次,因此使用 User.IsInRole(roleName) 仅涉及一次角色存储,而Roles.IsUserInRole(roleName)需要 N 次访问,其中 N 是网格中显示的用户帐户数。

如果访问此页面的用户是“管理员”或“监督者”角色,则“编辑”按钮的 Visible 属性设置为 True ;否则设置为 False。 仅当用户处于管理员角色时,“删除”按钮的 Visible 属性才会设置为 True

通过浏览器测试此页面。 如果以匿名访问者或既不是监督员或管理员的用户身份访问页面,则 CommandField 为空;它仍然存在,但作为一个薄片没有编辑或删除按钮。

注意

当非主管和非管理员访问页面时,可以完全隐藏 CommandField。 我把这个留作读者的练习。

非主管和非管理员的“编辑”和“删除”按钮处于隐藏状态

图 13:非主管和非管理员的“编辑”和“删除”按钮处于隐藏状态 (单击以查看全尺寸图像)

如果属于主管角色的用户 (但不属于管理员角色的用户) 访问,则他只看到“编辑”按钮。

虽然“编辑”按钮对主管可用,但“删除”按钮处于隐藏状态

图 14:虽然“编辑”按钮对主管可用,但“删除”按钮处于隐藏状态 (单击以查看全尺寸图像)

如果管理员访问,她有权访问“编辑”和“删除”按钮。

“编辑”和“删除”按钮仅适用于管理员

图 15:“编辑”和“删除”按钮仅适用于管理员 (单击以查看全尺寸图像)

步骤 3:将 Role-Based 授权规则应用于类和方法

在步骤 2 中,我们将编辑功能限制为“主管”和“管理员”角色的用户,而删除功能仅限管理员。 这是通过编程技术为未经授权的用户隐藏关联的用户界面元素来实现的。 此类措施不能保证未经授权的用户将无法执行特权操作。 可能存在稍后添加的用户界面元素,或者我们忘记为未经授权的用户隐藏。 或者,黑客可能会发现一些其他方法来获取 ASP.NET 页面以执行所需的方法。

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

我们在基于用户的授权教程中回顾了如何使用 PrincipalPermission 特性。 具体而言,我们了解了如何修饰 GridView 的 SelectedIndexChangedRowDeleting 事件处理程序,以便它们只能分别由经过身份验证的用户和 Tito 执行。 属性 PrincipalPermission 同样适用于角色。

让我们演示如何在 PrincipalPermission GridView RowUpdatingRowDeleting 事件处理程序上使用 特性来禁止对未经授权的用户执行。 只需在每个函数定义上添加相应的属性:

<PrincipalPermission(SecurityAction.Demand, Role:="Administrators")>_
<PrincipalPermission(SecurityAction.Demand, Role:="Supervisors")>_
Protected Sub UserGrid_RowUpdating(ByVal sender As Object, ByVal e As System.Web.UI.WebControls.GridViewUpdateEventArgs) Handles UserGrid.RowUpdating
     ...
End Sub

<PrincipalPermission(SecurityAction.Demand, Role:="Administrators")>_
Protected Sub UserGrid_RowDeleting(ByVal sender As Object, ByVal e As System.Web.UI.WebControls.GridViewDeleteEventArgs) Handles UserGrid.RowDeleting
     ...
End Sub

事件处理程序的 RowUpdating 属性规定,只有管理员或监督员角色中的用户才能执行事件处理程序,其中事件处理程序上的 RowDeleting 属性将执行限制为管理员角色中的用户。

注意

特性 PrincipalPermission 表示为 命名空间中的 System.Security.Permissions 类。 请务必在代码隐藏类文件的顶部添加语句 Imports System.Security.Permissions 以导入此命名空间。

如果非管理员尝试以某种方式执行 RowDeleting 事件处理程序,或者如果非监督器或非管理员尝试执行 RowUpdating 事件处理程序,.NET 运行时将引发 SecurityException

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

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

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

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

总结

在本教程中,我们了解了如何根据用户的角色指定粗略和精细的授权规则。 Asp。NET 的 URL 授权功能允许页面开发人员指定允许或拒绝哪些标识访问哪些页面。 正如我们在基于用户的授权教程中看到的那样,URL 授权规则可以按用户应用。 还可以按角色应用它们,如本教程的步骤 1 所示。

细粒度授权规则可以声明方式或编程方式应用。 在步骤 2 中,我们介绍了如何使用 LoginView 控件的 RoleGroups 功能根据访问用户的角色呈现不同的输出。 我们还了解了以编程方式确定用户是否属于特定角色的方法,以及如何相应地调整页面的功能。

编程快乐!

深入阅读

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

关于作者

Scott Mitchell 是多本 ASP/ASP.NET 书籍的作者和 4GuysFromRolla.com 的创始人,自 1998 年以来一直从事 Microsoft Web 技术工作。 Scott 担任独立顾问、培训师和作家。 他的最新一本书是 山姆斯在 24 小时内 ASP.NET 2.0。 可以在 上联系 mitchell@4guysfromrolla.com 斯科特,也可以通过他的博客在 联系 http://ScottOnWriting.NET

特别感谢...

本教程系列由许多有用的审阅者审阅。 本教程的主要审阅者包括 Suchi Banerjee 和 Teresa Murphy。 有兴趣查看我即将发布的 MSDN 文章? 如果是这样,请在 mitchell@4GuysFromRolla.com