基于角色的授权 (C#)

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

如果将角色框架配置为在 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
cookieRequireSSL 一个布尔值,指示传输身份验证 Cookie 是否需要 SSL 连接。 默认值为 false
cookieSlidingExpiration 一个布尔值,指示在单个会话期间每次用户访问站点时是否重置 Cookie 的超时。 默认值为 false。 仅当 设置为 truecreatePersistentCookie,此值才相关。
cookieTimeout 指定身份验证票证 Cookie 过期的时间(以分钟为单位)。 默认值为 30。 仅当 设置为 truecreatePersistentCookie,此值才相关。
createPersistentCookie 一个布尔值,指定角色缓存 Cookie 是会话 Cookie 还是持久性 Cookie。 如果 false (默认) ,则使用会话 Cookie,该 Cookie 在关闭浏览器时会被删除。 如果 true为 ,则使用持久性 Cookie;它将在创建该 Cookie 后或上次访问后的分钟数过期 cookieTimeout ,具体取决于 的值 cookieSlidingExpiration
domain 指定 Cookie 的域值。 默认值为空字符串,这会导致浏览器使用从中发出它 (的域,例如 www.yourdomain.com) 。 在这种情况下,向子域(例如,admin.yourdomain.com)发出请求时 ,不会 发送 Cookie。 如果希望将 Cookie 传递到所有子域,则需要自定义 domain 属性,将其设置为“yourdomain.com”。
maxCachedResults 指定 Cookie 中缓存的角色名称的最大数目。 默认值为 25。 RoleManagerModule不会为属于多个maxCachedResults角色的用户创建 Cookie。 因此, RolePrincipal 对象的 IsInRole 方法将使用 Roles 类来确定用户的角色。 存在的原因是 maxCachedResults ,许多用户代理不允许大于 4,096 字节的 Cookie。 因此,此上限旨在降低超出此大小限制的可能性。 如果角色名称非常长,可以考虑指定较小的 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 中,而不必在每个请求中查找用户的角色信息。 我分别将 和 属性显式设置为 createPersistentCookiefalseAllcookieProtection 从技术上讲,我不需要为这些属性指定值,因为我刚刚将它们分配给它们的默认值,但我将其放在此处是为了明确表明我未使用永久性 Cookie,并且 Cookie 已加密和验证。

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

注意

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

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

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

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

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

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

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

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

若要实现此目的,请首先将文件添加到 Web.configRoles 文件夹。

将 Web.config 文件添加到角色目录

图 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页进行此最终重定向。 具体而言,如果查询字符串包含参数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,并将其ID设置为 UserGrid。 稍后,我们将编写调用 Membership.GetAllUsers 方法并将生成的 MembershipUserCollection 对象绑定到 GridView 的代码。 包含MembershipUserCollectionMembershipUser系统中每个用户帐户的对象;MembershipUser对象具有 、 LastLoginDateUserNameEmail、 等属性。

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

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

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

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

对于“Email”TemplateField,将名为 的 Email TextBox 添加到其EditItemTemplate,并使用双向数据绑定将其Text属性绑定到 Email 属性。 将 RequiredFieldValidator 和 RegularExpressionValidator 添加到 ,EditItemTemplate以确保编辑 Email 属性的访问者输入了有效的电子邮件地址。 对于“注释”模板字段,将名为 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 void Page_Load(object sender, EventArgs e)    
{    
     if (!Page.IsPostBack)    
          BindUserGrid();    
}

private void BindUserGrid()    
{    
     MembershipUserCollection allUsers = Membership.GetAllUsers();    
     UserGrid.DataSource = allUsers;    
     UserGrid.DataBind();    
}

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

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

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

注意

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

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

首先,为 GridView 的 RowEditing、 和 RowUpdating 事件创建事件处理程序,RowCancelingEdit然后添加以下代码:

protected void UserGrid_RowEditing(object sender, GridViewEditEventArgs e)
{
     // Set the grid's EditIndex and rebind the data

     UserGrid.EditIndex = e.NewEditIndex;
     BindUserGrid();
}

protected void UserGrid_RowCancelingEdit(object sender, GridViewCancelEditEventArgs e)
{
     // Revert the grid's EditIndex to -1 and rebind the data
     UserGrid.EditIndex = -1;
     BindUserGrid();
}

protected void UserGrid_RowUpdating(object sender, GridViewUpdateEventArgs e)
{
     // Exit if the page is not valid
     if (!Page.IsValid)
          return;

     // Determine the username of the user we are editing
     string UserName = UserGrid.DataKeys[e.RowIndex].Value.ToString();

     // Read in the entered information and update the user
     TextBox EmailTextBox = UserGrid.Rows[e.RowIndex].FindControl("Email") as TextBox;
     TextBox CommentTextBox = UserGrid.Rows[e.RowIndex].FindControl("Comment") as TextBox;

     // Return information about the user
     MembershipUser UserInfo = 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();
}

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

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

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

protected void UserGrid_RowDeleting(object sender, GridViewDeleteEventArgs e)
{
     // Determine the username of the user we are editing
     string UserName = 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();
}

上述事件处理程序首先从 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>

除了 AnonymousTemplateLoggedInTemplate,LoginView 控件还可以包括 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 访问此页面时,他会看到“监督器”消息。

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

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

以编程方式限制功能

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

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

将 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 void UserGrid_RowCreated(object sender, GridViewRowEventArgs e)
{
     if (e.Row.RowType == DataControlRowType.DataRow && e.Row.RowIndex != UserGrid.EditIndex)
     {
          // Programmatically reference the Edit and Delete LinkButtons
          LinkButton EditButton = e.Row.FindControl("EditButton") as LinkButton;

          LinkButton DeleteButton = e.Row.FindControl("DeleteButton") as LinkButton;

          EditButton.Visible = (User.IsInRole("Administrators") || User.IsInRole("Supervisors"));
          DeleteButton.Visible = User.IsInRole("Administrators");
     }
}

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

如果处理的数据行未处于编辑模式,则会引用 Edit 和 Delete LinkButton,并根据对象的 方法返回UserIsInRole(roleName)布尔值设置其Visible属性。 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 void UserGrid_RowUpdating(object sender, GridViewUpdateEventArgs e)
{
     ...
}

[PrincipalPermission(SecurityAction.Demand, Role = "Administrators")]
protected void UserGrid_RowDeleting(object sender, GridViewDeleteEventArgs e)
{
     ...
}

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

注意

特性 PrincipalPermission 表示为 命名空间中的 System.Security.Permissions 类。 请务必在代码隐藏类文件的顶部添加语句 using 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