wind

a blog of erpcrm

 

[转发]设计由应用程序管理的授权

设计由应用程序管理的授权


2002年12月
模式和练习主页
Chris Schoon、Doug Rees、Edward Jezierski
Microsoft Corporation

摘要

本指南介绍为基于 Microsoft® .NET 的单层或多层应用程序设计和编写由应用程序管理的授权的指导原则,主要讨论常见的授权任务和方案,并提供相应的信息帮助您选择最佳方法和技术。本指南适用于体系结构设计人员和开发人员。

本指南假定读者已经了解 Windows 身份验证和授权、XML Web Service 以及 .NET Remoting 等主题的基本知识。有关设计分布式 .NET 应用程序的详细信息,请参阅 MSDN® Library 中的“Designing Applications and Services”。有关分布式应用程序安全设计的详细信息,请参阅 MSDN Library 中的 Building Secure ASP.NET Applications(英文)。有关其他的常规设计准则,请参阅 Microsoft TechNet 中的 .NET Architecture Center(英文)。

下载

单击此处下载本指南(PDF 格式)。

目录

本指南包括以下各节:

简介

本指南介绍如何在基于 .NET 的应用程序中实现授权,解释术语“授权”并讨论几种执行授权的机制。本指南还包括以下内容:

  • 标识和主体等重要概念。
  • 如何使用基于角色的安全设置来授权具有相同安全特权的一类用户。
  • 基于角色的 .NET 和 COM+ 安全设置之间的主要区别。

采用何种授权机制通常取决于验证用户身份(标识)的方法。本指南将探讨以下内容:

  • Windows 身份验证和非 Windows 身份验证之间的区别。
  • 这些身份验证机制如何影响授权。
  • 如何向远程应用程序层传递用于授权的标识信息。

在典型的企业应用程序中,需要在应用程序的不同层次执行不同类型的授权。为了帮助您识别各层的授权需要以及在不同的方案中选择合适的授权策略,本指南介绍在用户界面层、业务层和数据层中通常使用的典型授权任务。图 1 显示了企业应用程序的各个层上出现的一些重要授权问题。

图 1:在企业应用程序的各层中执行授权

.NET Framework 类库提供了多种接口和类,帮助您使用基于角色的 .NET 安全设置来执行授权。本指南介绍:

  • 几种检查用户是否属于某个特定角色的技术。
  • 如何处理授权错误。
  • 在多线程的 .NET 应用程序中出现的特殊授权问题。

定义授权框架时所做的大部分工作,都可以在多个应用程序中重复使用。本指南将对以下内容进行总结:

  • 如何定义可重复使用的授权框架。
  • 使框架的安全性和性能达到最佳的原则。
注意:本指南适用于使用 .NET Framework 功能进行由应用程序管理的授权。Microsoft Windows® Server 2003 系列操作系统中的授权管理器 API 和 Microsoft Management Console (MMC) 管理单元,为应用程序提供了具有完整的基于角色的访问控制框架。授权管理器 API 也称为 AzMan,它提供了一种简化的开发模型,用于管理灵活的分组和业务规则,并可用于存储授权策略。有关详细信息,请参阅 MSDN Library 中的 Authorization Interfaces(英文)和 Authorization Objects(英文)。

了解授权

“授权”是对通过验证的主体(用户、计算机、网络设备或程序集)是否具有执行某项操作的权限的确认。授权提供的保护只允许指定用户执行特定操作,并防止恶意行为。

本节的内容如下:

  • 授权提供的保护。
  • 基本授权。
  • .NET Framework 的授权功能。

降低安全威胁

仅有授权还不足以保证应用程序的安全,因此,本指南将简要介绍应用程序面临的几种威胁。以下是一些常见的安全威胁,这些威胁通常缩写为“STRIDE”,包括:

  • 标识欺骗 - 未授权的用户冒充应用程序的合法用户
  • 篡改数据 - 攻击者非法更改或毁坏数据
  • 可否认性 - 用户否认执行了操作的能力
  • 信息泄露 - 敏感数据被泄露给本应无权访问的人或位置
  • 拒绝服务 - 导致用户无法使用应用程序的破坏行为
  • 特权升级 - 用户非法获得过高的应用程序访问特权

您可以使用以下技术来解决 STRIDE 威胁:

  • 身份验证 - 严格的身份验证有助于减少标识欺骗。当用户登录到 Windows 或启动应用程序时,他(她)会输入“凭据”信息,如用户名和密码。Windows 使用 NTLM 或 Kerberos 等协议验证用户的凭据,并让用户登录到系统。应用程序则通常使用系统登录产品,或者以实现自定义身份验证作为授权的基础。有关身份验证的详细信息,请参阅 MSDN Library 中的 Building Secure ASP.NET Applications(英文)。
  • 授权 - 使用本指南介绍的授权技术来应对数据篡改和信息泄露的威胁。
  • 标识流 - 跨多台计算机部署的应用程序,有时需要在系统中的计算机之间传递代表通过验证的用户标识的信息。如果在第一台计算机中进行身份验证,而其他应用程序逻辑驻留在单独的计算机上,通常会用到标识流。有关标识流的详细信息,请参阅本指南后面的设计用于授权的标识流
  • 审核 - 记录已授权的和未授权的操作,减少可否认性带来的威胁。本指南不对审核进行详细介绍。有关审核的详细信息,请参阅 MSDN Library 中的 Building Secure ASP.NET Applications(英文)。

有关 STRIDE 的详细信息,请参阅 MSDN Library 中的 Designing for Securability(英文)。

图 2 显示的模型说明了如何降低多层应用程序中的 STRIDE 安全威胁。

图 2:多层应用程序的安全模型

图 2 描述的是一种具有多个物理层的部署,但是,许多较小的应用程序是在一个物理层上完成的,这就简化了身份验证、授权和标识流。图 2 包含了以下降低安全威胁的措施:

  1. 通过带宽限制来降低拒绝服务 (DoS) 的攻击。这可以防止恶意的应用程序和用户对应用程序连续进行不受欢迎的干扰,从而避免应用程序出现问题。
  2. 使用加密来保证通信安全。
  3. 身份验证可以防止标识欺骗。
  4. 身份验证根据数据存储库来验证凭证。
  5. 应用程序层之间使用标识流(可选)。
  6. 通过审核来保证可否认性。
  7. 通过授权来应对篡改和泄露数据的威胁。

选择授权机制

您可以使用多种授权机制来控制应用程序的功能,使其按预期方式运行,不被意外或蓄意误用。这些授权机制包括以下几种:

  • 系统授权 - Windows 使用访问控制列表 (ACL) 保护资源,如文件和存储过程。ACL 指定哪些用户可以访问安全资源。
  • .NET 代码访问安全性授权 - 代码访问安全性根据代码的来源授权代码以执行操作。例如,代码访问安全性根据证据标准,确定 .NET 应用程序中可以访问其他程序集的程序集。
  • 应用程序授权 - 应用程序代码自身授权用户操作。

可以综合使用这些方法创建安全的应用程序,如图 3 所示。

图 3:选择授权机制

系统授权

“系统授权”是为操作系统控制的对象(例如打印机、文件)设置资源权限或 ACL 的过程。系统管理员负责维护这些设置。系统授权是一种非“是”即“否”的决策模式:用户要么被授权访问资源,要么不被授权访问资源。

系统授权的示例包括:

  • 基于 Microsoft ASP.NET 的应用程序授权设置,用于限制对 Web.config 文件中指定的 URL 文件或路径的访问。
  • Active Directory® 目录服务中的权限设置。
  • NTFS 文件系统访问控制项。
  • 消息排队权限。
  • 服务器产品(如 Microsoft SQL Server™)中授予的权限。这种权限可能涉及各种对象,如表或视图。

有关系统级安全和授权的详细信息,请参阅 MSDN Library 中的 Building Secure ASP.NET Applications(英文)。

系统授权可以对各种对象施加约束,而限制代码则需要采用 .NET 代码访问安全性授权。

.NET 代码访问安全性授权

.NET 公共语言运库使用代码访问安全性来限制可执行的代码。代码访问安全性根据证据向应用程序代码授予权限(称为“权限设置”)。这些证据可以包括代码的来源、发布者或其他证据,如程序集的严格名称。

权限设置使您能够控制应用程序可以执行的操作,如删除文件或访问 Internet。例如,您可以限制应用程序只使用隔离的存储单元或控制打印访问。

不管用户的身份如何,代码访问安全性只考虑证据,即使具有管理特权的用户使用应用程序,代码访问安全性权限仍旧保持不变。例如,如果代码来自 Internet,不管用户是谁,对其应用的限制(如删除文件的能力)保持不变。

代码访问安全性的应用示例包括:

  • 限制下载组件的操作权利并将其放置到隔离环境中,防止下载组件执行危险操作。隔离有助于防止欺诈代码损坏系统。
  • 为在 Web 服务器或应用程序服务器上运行的宿主代码创建隔离环境。
  • 限制组件的操作权利,防止被恶意代码误用。
注意:请使用 Caspol.exe 或 Microsoft .NET Framework 配置管理控制台配置代码访问安全性。

有关代码访问安全性的详细信息,请参阅 MSDN Library 中《.NET Framework Developer's Guide》中的 Code Access Security(英文)一文。

代码访问安全性通过检查代码权限来保证系统的安全性,但可能还需要使用应用程序授权来检查用户的权限,这取决于应用程序。

应用程序授权

大多数应用程序会根据用户与系统的交互活动实现不同的功能或安全权限。设计“应用程序授权”指根据程序中的用户角色,实施业务规则或限制用户对应用程序资源的访问。

应用程序授权的主要目的是保护功能和其他无形内容,如业务逻辑。因此,很难使用当前的系统级技术实现应用程序授权,因为这些技术需要使用有关物理资源的设置,如 ACL。例如,您想确保对员工费用申请的批准操作的安全,却没有要保护的物理资源,因此,当您设计应用程序授权时,应该着眼于高级别的操作,而不是各种资源。

当系统授权机制分类过细或不考虑应用程序的数据与状态时,应用程序授权提供了另一种系统授权方法。例如,如果 XML Web Service 的系统级安全标准仍处于开发阶段,还在不断地发展丰富,那么您可以不必等到标准形成之后再向 XML Web Service 添加安全设置或创建安全的 XML Web Service。对于目前已创建的 XML Web Service,您可以实现应用程序授权,使用安全套接字层 (SSL) 或其他组合来保护对服务的调用。

应用程序授权的示例包括:

  • 检查用户是否具有执行应用程序中特定操作(如批准费用申请)的权限。
  • 检查用户是否具有访问应用程序资源(如从数据库中检索敏感数据列)的权限。

在本指南的后面部分中,您将学习如何设计这些应用程序授权以及如何编写相关代码。

入口检查

为了防止操作被连续不断地错误执行而导致最终失败,您应该始终做到尽快地对用户的每个请求进行授权。每个授权点称为“看门人”。这种看门机制的示例包括 ASP.NET 入口中的文件和 URL 授权。在标识流向各个层次传递的过程中,可能会有若干个“看门人”。在门口进行检查可以减少在系统深层(通过入口点或门以后)必需的授权检查次数。

在系统深层执行授权检查的对象需要较少的授权失败补偿逻辑。单个组件不负责处理授权失败,不会抛出异常来通知失败的调用者。

使用角色执行授权

.NET Framework 提供了基于角色的应用程序授权能力。“角色”指共享同一安全特权的一类或一组用户。

使用角色代替特定的用户标识具有以下优势:

  • 发生变化(如添加用户、提升用户或用户调离工作)时,不需要改变应用程序。
  • 维护角色的权限比维护各个用户的权限容易。例如,处理 10 个角色比处理 120 个用户容易,而且还节省时间。
  • 一个用户可以具备多个角色,这增强了分配和测试权限的灵活性。

根据业务组织定义角色

角色可以代表用户在组织中的地位,例如:

  • Manager
  • Employee
  • ClaimApprovalDepartment

使用这种方法的一个好处是信息通常可以从存储库(例如 Active Directory)中检索出来。通常情况下,这些角色在对实际业务需求进行建模时十分有用。

与组织的变化无关

您还可以使用角色来指出用户执行的工作属于哪种操作类型。这样的角色可以将应用程序的功能链接到各个用户,例如:

  • CanApproveClaim
  • CanAccessLab
  • CanViewBenefits

第二种方法更灵活一些,因为您可以围绕应用程序的功能来设计角色,而不用过多考虑组织的结构,但维护起来可能比较困难,因为缺乏保存角色的结构。大多数情况下,需要在应用程序中综合使用这些方法。

不使用角色执行授权

有时您必须以用户是谁作为授权的基础,而不会过分关注用户在应用程序中扮演的角色。例如,您可能要实现只允许部门经理批准员工的费用申请,而要达到这种授权级别,可以将当前用户与提出申请的员工的经理进行比较。

在基于 .NET 的应用程序中使用基于角色的安全设置

.NET Framework 在 System.Security.Principal 命名空间中提供了基于角色的安全设置实现,您可以用此实现来授权应用程序。要在 .NET Framework 中使用应用程序授权,请创建 IIdentityIPrincipal 对象来表示用户。IIdentity 封装的是一个通过验证的用户,IPrincipal 则是用户标识和用户角色的组合。

图 4 显示了 IIdentityIPrincipal 对象之间的关系。

图 4:IIdentity 和 IPrincipal 对象之间的关系

请注意图 4 中的以下几点:

  1. IIdentity 对象是实现 IIdentity 类的实例。IIdentity 对象表示特定的用户。
  2. IIdentity 接口具有 NameIsAuthenticatedAuthenticationType 属性。实现 IIdentity 的类通常还包含有特定用途的其他私有成员,例如,WindowsIdentity 类封装了正运行代码的用户的帐户令牌。
  3. IPrincipal 对象是实现 IPrincipal 的类的实例。IPrincipal 对象是代表用户的 IIdentity 及其具有的角色的组合。这样就可以实现单独的身份验证和授权。
  4. IPrincipal 对象使用 IsInRole 方法执行授权,并通过 Identity属性提供对 IIdentity 对象的访问。

使用标识

.NET Framework 提供了四种实现 IIdentity 接口的类:

  • WindowsIdentity
  • GenericIdentity
  • PassportIdentity
  • FormsIdentity

每种类都允许您使用不同种类的用户标识。要访问使用 Windows 身份验证的应用程序的当前 WindowsIdentity 对象,可以使用 WindowsIdentity 类的静态 GetCurrent 方法,如以下代码所示:

您还可以通过在自己定义的类中实现 IIdentity 接口来创建自定义标识类。有关创建自定义标识的详细信息,请参阅本指南后面的扩展默认实现。有关如何使用默认 IIdentity 实现的详细信息,请参阅本指南后面的设计用于授权的身份验证

使用主体

.NET Framework 提供了链接用户角色和标识的 IPrincipal 接口。所有执行应用程序授权的托管代码都应该使用实现 IPrincipal 的类的对象。例如,WindowsPrincipalGenericPrincipal 类提供了内置的 IPrincipal 实现。另外,您也可以根据 IPrincipal 创建自己的自定义主体类。

为了提高编码效率,您可以使用本指南的设计用于授权的身份验证一节中介绍的技术,通过使用 Thread 对象的静态 CurrentPrincipal 属性可以将 IPrincipal 对象链接到线程,这样当前线程就可以轻松地访问 IPrincipal 对象了,如以下代码所示:

然后,您可以测试用户是否属于某一特定的角色,从而执行授权检查。为此,可以使用 IPrincipal 接口的 IsInRole 方法,如以下代码所示:

ASP.NET 应用程序处理 IPrincipal 对象的方法与其他基于 .NET 的应用程序的处理方法有所不同。ASP.NET 通过无状态的 HTTP 协议创建会话外观。作为会话的一部分,执行用户请求的所有代码可以通过 HttpContext 对象的 User 属性,使用代表用户的 IPrincipal 对象。Global.asax 文件发生 OnAuthenticate 事件之后,公共语言运库使用 HttpContext.User 值自动更新 Thread.CurrentPrincipal

ASP.NET 应用程序通常使用 User 属性执行授权检查,如以下代码所示:

注意:手动更改 HttpContext.User 将自动更新在同一 HTTP 上下文环境中执行的所有线程的 Thread.CurrentPrincipal。但是,改变 Thread.CurrentPrincipal 不会影响 HttpContext.User 属性,它只影响为请求中其余内容选择的线程。

有关创建自己的 IPrincipal 类型的详细信息,请参阅本指南后面的扩展默认实现。有关如何使用默认 IPrincipal 实现的详细信息,请参阅本指南后面的设计用于授权的身份验证

授予使用 IIdentity 和 IPrincipal 对象的权限

使用 IIdentity 对象是一种敏感操作,因为在该操作中可以使用与用户相关的信息。允许应用程序更改当前的 IPrincipal 对象也应该受到保护,因为应用程序授权能力是以当前的主体为基础的。框架要求这些操作具有代码访问安全性权限,从而提供了保护。使用 Caspol.exe 或 .NET Framework 配置工具为需要管理这些对象的应用程序授予 SecurityPermissionAttribute.ControlPrincipal 权限。

默认情况下,所有本地安装的应用程序均具有该权限,因为它们是在“完全信任”的权限设置下运行的。

执行以下方法需要 ControlPrincipal 权限:

  • AppDomain.SetThreadPrincipalPolicy()
  • WindowsIdentity.GetCurrent()
  • WindowsIdentity.Impersonate()
  • Thread.CurrentPrincipal()

有关使用 CASPOL 设置安全权限的详细信息,请参阅 MSDN Library 中的 Configuring Security Policy Using the Code Access Security Policy Tool (Caspol.exe)(英文)。

Windows 和公共语言运库之间管理授权的区别

公共语言运库在 Windows 安全结构之上有一个单独的安全结构。Windows 线程具有 Windows 授权用户的令牌,而运行时线程具有代表该用户的 IPrincipal 对象。

因此,在开发代码时,必须至少考虑两种代表用户的安全上下文。例如,设想一个使用窗体身份验证的 ASP.NET 应用程序:默认情况下,ASP.NET 进程在名为“ASPNET”的 Windows 服务帐户(专为应用程序创建的用户帐号)下运行。假设有一位名为 Bill 的用户登录到 Web 站点。Thread.CurrentPrincipal 属性代表窗体身份验证的用户 Bill。公共语言运库看到以 Bill 运行的线程,于是所有托管代码都将 Bill 看作主体。如果托管代码要求访问系统资源(如文件),则不管 Thread.CurrentPrincipal 的值如何,非托管代码都可以看到 ASPNET Windows 服务帐户。图 5 说明了公共语言运库和操作系统线程之间的授权关系。

图 5:公共语言运库和操作系统线程之间的授权关系

模拟允许您改变操作系统线程在与 Windows 的交互过程中执行的用户帐户。可以通过调用 WindowsIdentity.Impersonate 方法模拟 Windows 标识。这种模拟形式允许您作为特定用户访问本地资源。当调用 Impersonate 方法时,您就以 WindowsIdentity 所代表的用户登录。您必须有权访问服务器中的用户凭据且有权调用 LogonUser API。但是,手动创建 WindowsIdentity 的过程比较复杂,因此,如果不是十分必要,最好不要执行模拟。

当应用程序代码模拟其他用户之后,WindowsImpersonatationContext.Undo 方法将用户标识还原到原始标识。这些函数一般必须成对调用,如以下代码所示:

有关模拟 Windows 帐户的详细信息,请参阅 MSDN Library 中的 Impersonating and Reverting(英文)。

注意:在 Windows 2000 中,调用 LogonUser API 的进程必须具备 SE_TCB_NAME 特权。有关验证用户凭据的详细信息,请参阅文章 Q180548 HOWTO: Validate User Credentials on Microsoft Operating Systems(英文),位于 Microsoft Knowledge Base(英文)中。

设计用于授权的身份验证

授权取决于身份验证,也就是说,必须对用户或过程进行身份验证,然后才能对其授权,以便查看或使用受保护的资源。本节将分析两种身份验证机制并阐述这两种机制对授权的影响:

  • 根据 Windows 身份验证执行授权
  • 根据非 Windows 身份验证执行授权

选择哪种身份验证机制通常受与授权无关的因素的影响,例如 Windows 用户帐户的可用性以及一些常见的环境问题,包括客户端的浏览器类型。但是,您选择的身份验证机制确实会影响您执行授权的方式。

根据 Windows 身份验证执行授权

所有应用程序在称为“Windows 标识”的 Windows 用户帐户下执行。在 Windows 身份验证过程中,您将使用这种 Windows 用户帐户或 Windows 用户所属的角色执行您的授权检查。Windows 根据由以下技术提供的凭据来选择用户帐户:

  • 使用用户登录到计算机时提供的信息
  • 使用通过配置工具(如组件服务管理实用程序)专为应用程序创建的服务帐户

使用 Windows 身份验证进行授权的整个过程是这样的:

  1. Windows 使用 NTLM 或 Kerberos 验证用户的凭据。有关使用 Kerberos 委托的详细信息,请参阅 Building Secure ASP.NET Applications(英文)中的“How To: Implement Kerberos Delegation for Windows 2000”。
  2. 如果登录成功,将生成一个 Windows 访问令牌,对 .NET 应用程序来说,该令牌封装在 WindowsIdentity 中。
  3. 运行时通常会自动为包含 Windows 角色概念的 WindowsPrincipal 对象设置 Thread.CurrentPrincipal 属性,但有些情况下,您必须亲自按本节后面介绍的方法进行设置。
  4. 现在可以使用 Thread.CurrentPrincipal 进行授权。
  5. 当您远程调用应用程序逻辑时,有时需要手动向被调用者提供标识信息。本指南后面的实现手动标识流将详细讨论该问题。
注意:先将 IPrincipal 对象链接到线程,然后再从 Thread.CurrentPrincipal 属性检索主体或标识信息。这样就可以在内存中维护一个角色集,从而降低查找这类信息的频率。

使用 Windows 角色执行授权

您可以使用 WindowsPrincipal 对象查看用户是否属于特定的 Windows 用户组(例如 “<domain>\Users”)。WindowsPrincipal 对象具有 Identity 属性,该属性返回一个 WindowsIdentity 对象,描述当前用户的标识。

您可以配置存储在 ASP.NET 中的 .NET 应用程序,使其通过启用 Web.config 中的 Windows 身份验证,从 Thread.CurrentPrincipal 属性自动访问 WindowsPrincipal。您可以在 ASP.NET Web 应用程序、XML Web Service 以及存储在 ASP.NET 中的 .NET Remoting 对象中使用该技术。

其他 .NET 应用程序,如 Windows 服务、控制台和基于 Windows 窗体的应用程序,需要您使用以下方法之一建立 Thread.CurrentPrincipal

  • 调用 SetPrincipalPolicy
  • 设置 CurrentPrincipal
注意:使用 IIdentityIPrincipal 对象的代码必须具备 ControlPrincipal 权限,请使用代码访问安全性为应用程序授予该权限。有关详细信息,请参阅本指南前面的在基于 .NET 的应用程序中使用基于角色的安全设置

调用 SetPrincipalPolicy

AppDomain 对象的 SetPrincipalPolicy 方法可以将特定类型的 IPrincipal 对象链接到应用程序域。请使用 PrincipalPolicy 枚举类型的 WindowsPrincipal 值,将当前的 WindowsPrincipal 对象链接到应用程序线程,如以下代码所示:

然后就可以授权检查主体了。在开始执行应用程序时,调用 SetPrincipalPolicy 以确保在执行任何授权检查之前 IPrincipal 对象已链接到线程。

请勿在 ASP.NET 应用程序中使用 SetPrincipalPolicy,因为在 Global.asax 中定义的 Application_AuthenticateRequest 事件处理程序过程中,ASP.NET 身份验证将为您管理此值。

注意:只有在应用程序域中不存在默认的主体时,SetPrincipalPolicy 才有效。

设置 CurrentPrincipal

要设置 CurrentPrincipal 属性,首先必须创建一个新的 WindowsPrincipal 对象,方法是将 WindowsIdentity 对象传递给 WindowsPrincipal 构造函数。然后将新的 WindowsPrincipal 对象链接到 Thread.CurrentPrincipal 属性,如以下代码所示:

然后就可以授权检查 IPrincipal 对象了。

有关如何使用 WindowsPrincipal 执行授权检查的详细信息,请参阅本指南后面的执行授权检查

使用应用程序定义的角色执行授权

要根据应用程序定义的角色(如 CanApproveClaims)创建授权检查,必须手动创建通用的或自定义的 IPrincipal 对象。您无需调用 SetPrincipalPolicy 方法,因为它的默认设置是 NoPrincipal,正是指定您自己的主体的正确设置。

大多数情况下,根据已通过身份验证的用户名来创建 GenericIdentity 对象。(通过使用 IIdentity 接口的 Name 属性,可以得到该用户名。)这样,在向其他组件提供 IIdentity 对象时,就可以避免所有涉及用户登录令牌(可以使用 WindowsIdentity 对象访问)的安全问题。然后,可以创建 GenericPrincipal,将 IIdentity 对象链接到自己的应用程序定义的角色列表中。

以下代码显示了使用 Windows 身份验证时,如何创建 GenericIdentityGenericPrincipal 对象:

大多数情况下,应该从数据存储库中检索应用程序定义的角色,而不是为角色编写代码,从而在指定角色成员时获得更大的灵活性。有关使用该方法的示例,请参阅附录中的如何使用 SQL Server 构建 GenericPrincipal

有关如何使用 GenericPrincipal 执行授权检查的详细信息,请参阅本指南后面的执行授权检查

根据非 Windows 身份验证执行授权

您可能不想或不能使用 Windows 身份验证执行应用程序授权。出现这种情况的原因可能是用户没有 Windows 用户帐户,或者是因为技术限制而无法使用 Windows 身份验证来验证用户的凭据。

非 Windows 身份验证是使用非 Windows 技术验证用户凭据的过程。这种身份验证包括 .NET Framework 为您提供的验证(如 ASP.NET 窗体身份验证)和您自己创建的验证方法。

使用非 Windows 身份验证进行授权的整个过程如下:

  1. 应用程序收集用户凭据。
  2. 应用程序验证凭据,通常是以自定义的数据存储库(如 SQL Server 数据库)为参照。
  3. 开始是将 Thread.CurrentPrincipal 属性设置为不包含任何角色的 GenericPrincipal 对象。然后用新的 GenericPrincipal 对象,或者用实现 IPrincipal 的自定义类对象取代 Thread.CurrentPrincipal,以提供应用程序定义的角色。
  4. 现在可以使用 Thread.CurrentPrincipal 进行授权。
  5. 当远程调用应用程序逻辑时,需要向被调用者手动提供标识信息。

常见的非 Windows 身份验证类型有:

  • ASP.NET 窗体身份验证 - ASP.NET 窗体身份验证自动创建 FormsIdentity 对象,代表已验证的标识。这种验证包含一个 FormsAuthenticationTicket,提供用户身份验证会话的信息。

    窗体身份验证提供程序根据 FormsIdentity 对象创建 GenericPrincipal,但是没有角色成员关系。

    有关如何使用 ASP.NET 窗体身份验证的详细信息,请参阅文章 Q301240 HOW TO: Implement Forms-Based Authentication in Your ASP.NET Application by Using C# .NET(英文),位于 Microsoft Knowledge Base(英文)中。

    窗体身份验证几乎总是使用应用程序特定的凭据。有关如何使用窗体身份验证方式来验证获得的 Windows 凭据的示例,请参阅文章 Q316748 HOW TO: Authenticate Against the Active Directory by Using Forms Authentication and Visual C# .NET(英文),位于 Microsoft Knowledge Base(英文)中。

  • Passport 身份验证 - ASP.NET 应用程序可以使用 .NET Passport 执行身份验证。Passport 身份验证生成一个表示用户标识的 PassportIdentity 对象。PassportIdentity 对象扩展了基本的 IIdentity 接口,以便包括 Passport 配置文件信息。

    Passport 身份验证提供程序根据 PassportIdentity 对象创建一个 GenericPrincipal,但是没有角色成员关系。

    有关 Passport 身份验证的详细信息,请参阅 MSDN Library 中的 The Passport Authentication Provider(英文)。

  • ISAPI 身份验证解决方案 - 是一种备用方法,它使用 HTTP 标头来实现手动或自定义的身份验证机制,例如 Microsoft Commerce Server 中的身份验证机制。有关 Microsoft Commerce Server 中安全设置的详细信息,请参阅 MSDN Library 中的 Commerce Server Security(英文)。

    HTTP 传输提供用于构建通用或自定义的 IIdentity 对象和 IPrincipal 对象的信息(如 cookie)。

有关 ASP.NET 的身份验证技术的详细信息,请参阅 MSDN Library 中的 Authentication in ASP.NET: .NET Security Guidance(英文)。

以上列举的所有非 Windows 身份验证类型均涉及到 GenericPrincipal 对象或自定义 IPrincipal 对象。关于如何将 GenericPrincipal 链接到应用程序定义的角色的示例,请参阅附录中的如何使用 SQL Server 构建 GenericPrincipal

设计用于授权的标识流

身份验证创建用于授权的 IIdentityIPrincipal 对象,并确定如何通过编程方式将标识信息传递给其他计算机上远程部署的应用程序逻辑。已通过验证的标识的传递过程称为“标识流”。实现标识流的方法有两种:

  • 自动标识流
  • 手动标识流

使用自动标识流

如果所有需要标识信息的代码是在同一上下文中执行的,公共语言运库将自动提供标识流。调用者和被调用者共享相同的应用程序域时,它们处于同一上下文。如果客户端代码(称为“调用者”)和被调用的组件(称为“被调用者”)在同一上下文中运行,.NET 将对两者自动使用同一个 Thread.CurrentPrincipal 对象。有关被调用者和调用者处在不同线程的情况,请参阅本章后面的使用多线程执行授权

在一个应用程序域中执行的代码示例包括:

  • 在进程内组件中包含所有必需的代码且未远程部署逻辑的 ASP.NET Web 站点。
  • 未远程部署逻辑、基于 Windows 窗体的单机应用程序。

实现手动标识流

由于技术限制,需要在不能执行身份验证的远程层进行授权检查时,可以实现手动标识流。例如,通过 TCP 通道使用 .NET Remoting 调用远程组件。有关在 .NET Remoting 应用程序中使用通道的详细信息,请参阅 MSDN Library 中的 Microsoft .NET Remoting: A Technical Overview(英文)。

如果您正在实现自己的标识流,请使用与调用代码不同的方式传递数据(称为“分离”)。如果您的组件接口涉及调用功能,并且使用参数传递数据,请使用其他方式发送标识信息。

例如,如果您正在使用 SOAP 调用 XML Web Service,则请勿将标识信息作为消息正文的一部分发送,而应将其放入自定义的 SOAP 标头中。这时对安全性的要求不象处理代码设计问题时那样严格。这种分隔方式可以增加代码接口的扩展性、可复用性和聚合性。这样做还使您可以随着技术标准的发展更改发送标识信息的方法,而无需改变接口约定。Web Services Security (WS-Security)(英文)规范说明了这一点。

请注意,标识流不同于传递凭据,后者是在服务器上进行的高效重验证过程。另外,您还可以在消息本身中传递已加密的凭据,但是,加密需要使用共享密钥或公钥系统,所以用这种方式传递凭证没有什么优势。

以下是一些实现标识流的最佳方案:

  • 使用功能要求和技术限制允许的最有力的身份验证。
  • 有些标识流技术会涉及存储和/或传递机密信息,这些信息对于标识流机制的安全性至关重要。对机密信息进行加密或者使用安全的通道来保证信息的安全。System.Security.Cryptography 命名空间中的类或许能对您有所帮助。另外,Building Secure ASP.NET Applications(英文)中的“How To: Create a DPAPI Library”的示例也可以供您借鉴。
  • 使用诸如数据保护 API (DPAPI) 之类的技术保存机密信息。有关 DPAPI 的信息,请参阅 MSDN Library 中的 Windows Data Protection(英文)。
  • 确保您的实现允许您的审核达到所需的深度。有关审核的详细信息,请参阅 Microsoft TechNet 中的 Monitoring and Auditing for End Systems(英文)。

在信任环境中传递标识信息

在信任环境中,被调用者将身份验证的责任交给了调用者。这时认为我们从调用者传递到被调用者的所有信息都是安全可靠的,例如 Web 层是调用者,内部应用程序中的组件是被调用者。

注意:如果非法用户可以调用其他(和潜在虚假)标识传递的代码,这种解决方案将是不安全的。为了防止这类欺诈攻击,请使用系统层提供的技术来保护安全套接字层 (SSL) 或 IP 安全性 (IPSec) 通信通道,并使用代码访问安全性来确保只有相应的代码才能调用您的函数。

在信任环境中,主要采用两种方法在应用程序层之间传递标识信息:

  • 只传递用户名 - 调用者只向被调用者传递用户名。然后,如本指南前面提到的那样,被调用者查询授权存储库,创建通用的或自定义的 IPrincipal 对象,并将对象连接到线程中。

    当您向数据库传递标识信息进行审核时,也可以使用这种方法。有关详细信息,请参阅本指南后面的在数据层执行授权

    该方法较为灵活,因为不需要强制调用者传递特定类型的对象或额外信息,例如应用程序角色。

  • 传递角色(使用 IPrincipal 对象)- 验证用户标识之后,调用者通过序列化通用或自定义 IPrincipal 对象向被调用者传递角色。然后,如前面提到的那样,被调用者反序列化对象,并将对象连接到线程中。这与在单个应用程序的域中自动发生的过程的效果相同。。

    通过传递自定义的 IPrincipal 对象,您可以传递角色、自定义的 IPrincipal 对象包含的任意附加信息(如用户配置文件)以及标识。

    但是,不能传递封装了某种信息的 IPrincipal 对象。例如,不能传递 WindowsPrincipal 或其他所有以 WindowsIdentity 对象为基础的主体,因为这些标识信息中包含的令牌在其原始环境之外是没有意义的。

    有关序列化 IPrincipal 对象的详细信息,请参阅附录中的如何在 .NET Remoting 组件中启用授权

注意:只有在使用安全通信通道的信任环境中(如使用防火墙保护的某个公司 LAN),才可以传递标识信息,以免网络数据在网络上被截获或修改而遭到欺诈。

在非信任环境中传递标识信息

非信任环境是指调用者不信任被调用者的情形。在非信任环境中,被调用者必须验证调用者,然后才执行授权,例如 Web 应用程序验证浏览器的所有请求。

在非信任环境中的应用程序层之间传递标识信息有两种常见的方法:

  • 传递签名数据 - 使用公钥结构传递加密的签名数据,是传递标识流的一种有效方法。由于不存在身份验证会话,这种方法通常是和异步通信一起使用的。

    传递签名数据包括对消息进行数字签名。数字签名是根据发件人的私钥和消息内容计算出的。为了实现验证,被调用者通过密钥管理系统获得发件人的公钥,并使用该密钥确定发件人在签署消息时是否使用了相应的私钥。

    .NET Framework 提供的 SignedXml 类可以作为 XMLDSIG 标准的实现。该标准不涉及密钥管理或密钥信任等问题。收件人不必假设对该过程所使用的密钥的信任程度,这部分由应用程序代码负责。有关 XMLDSIG 标准的详细信息,请参阅 W3C 网站上的 XML-Signature Syntax and Processing(英文)。

    有关如何使用 SignedXml 类的代码示例,请参阅 CAPICOM SDK。CAPICOM 是一种 Microsoft ActiveX® 控件,提供到 Microsoft CryptoAPI 的 COM 接口。该代码示例说明了使用证书授权、签名和验证 XMLDSIG 签名的全过程。您可以从 samples\c_sharp\xmldsig 目录中的 Platform SDK Redistributable: CAPICOM 2.0 找到该代码示例。有关 CAPICOM 的详细信息,请参阅 MSDN Library 中的 CAPICOM Reference(英文)。

  • 传递令牌 - 传递令牌会传递一些信息,被调用者要验证这些信息以确定用户标识。

    传递令牌要求使用共享的身份验证机制或确定令牌有效性的方法。传递令牌可能包括在组件之间传递 Kerberos 票据。另一个例子是电子商务网站中 Internet Information 服务 (IIS) 应用程序根据身份验证创建的会话 cookie。每次浏览器发出调用命令时,cookie 都会传递回 Web 服务器,这是典型的 ISAPI 解决方案。

上面您已经了解了如何使用各种身份验证机制实现授权,以及如何在各层之间传递授权数据,下面将为您介绍如何在企业应用程序的各层进行授权。

在企业应用程序中执行授权

目前大多数的应用程序都得益于多层设计,因为多个层次可以提供伸缩性、灵活性并且可以改善性能。尽管应用程序各层的授权目的相同(控制用户访问系统功能和数据),但是各层授权的设计方法和实现方法并不相同。企业应用程序中的三层是:

  • 用户界面层
  • 业务层
  • 数据层

在用户界面层执行授权

本节介绍在用户界面层执行授权的方法。本指南的后面将介绍以下主题:在业务层执行授权在数据层执行授权

在用户界面层执行应用程序授权,有助于确保只有得到授权的用户才可以查看和修改数据,或者执行与特定岗位相关的业务功能(例如与工资有关的操作)。对于还需要在系统的其他层进行授权的过程来说,在用户界面层授权用户,是限制对这些过程的访问的第一道关卡。

要实现以下目的时,请在用户界面层创建访问检查:

  • 根据用户的角色成员关系,启用或禁用以及显示或隐藏控件。有关详细信息,请参阅本节后面的改变窗体中的控件
  • 根据用户的角色成员关系,将流从一个窗体(或页面)更改到下一个窗体(或页面)。有关详细信息,请参阅本节后面的改变页面流

在用户界面层创建授权代码时,请使用以下最佳方案:

  • 只要可能,就配置系统授权,以控制用户界面的输入。例如,配置 ASP.NET 应用程序中的 Web.config 文件,只允许得到授权的用户访问输入页。有关 ASP.NET URL 授权的详细信息,请参阅 MSDN Library 中的 Building Secure ASP.NET Applications(英文)。
  • 在加载窗体或页面时对用户授权,以防止用户在进程或任务出现错误时访问用户界面元素。例如,要求用户在 ASP.NET 应用程序中填写多个网页之后才能完成操作。如果允许用户在浏览器中直接输入 URL,可能导致用户能够访问那些在特定阶段无权访问的页面。因此,加载页面时应执行应用程序授权检查。
  • 请勿只将系统的安全设置建立在用户界面中的应用程序授权之上。在业务层和数据层要执行其他的访问检查,因为有可能会以多种方式或从不同的应用程序中调用这些组件。
  • 如果用户界面限制用户可以查看的数据,则请勿从数据源读取不必要的数据,这样可以避免受保护的数据在网络传输中被泄露。
注意:以下主题的所有代码示例均以 Web 窗体为例。可以通过将 User.IsInRole 更改为 Thread.CurrentPrincipal.IsInRole,使代码适用于 Windows 窗体。有关 IsInRole 方法和执行授权检查的详细信息,请参阅本指南后面的执行授权检查

改变窗体中的控件

在 Windows 窗体和 ASP.NET 应用程序中,都有可能需要根据授权改变控件在用户界面中的显示方式,包括改变某些控件的可见性或者禁用它们。

例如,在雇员费用申请的应用程序中,CanApproveClaims 角色的成员需要在窗体中使用一个按钮批准申请,而所有不属于该角色的用户都看不到这个 Approve 按钮。以下代码说明实现该目的的方法:

如果用户界面比较复杂或者有很多自定义的角色,那么在创建授权代码时,以上这种过于简单的方法会导致“if-then-else”语句链过长。

您可以使用以下技术来降低这种根据授权检查显示控件的复杂性:

  • 合并逻辑 - 将改变控件的逻辑放到从窗体的 Load 事件中调用的私有方法内。这样,就只用一个位置保存有关显示控件的窗体授权逻辑。

    以下代码显示了如何对 Web 窗体应用这种方法:

  • 必要时使用中继器控件 - 如果控件与多行数据相对应,可以使用中继器控件,从而能够同时修改所有的控件设置。

    例如,如果您创建了一个使用 DataGrid 的 Web 窗体,则可以使用一个复选框模板列,允许已授权的用户同时批准多项申请。

    以下代码说明了如何使用该技术在 DataGrid 中显示 Approve 列。

  • 使用模型视图控制器模式 - 模型视图控制器模式可以帮助您从控件激发的事件中分离控制用户界面元素行为的逻辑。在费用申请示例中,窗体可以包含一个员工列表框和一个 Show Claims 按钮。在按下 ShowClaims 时,通过将所有应执行的代码放到另一个函数中(叫做控制器函数),甚至是另一个类中,可以强制实现清楚的逻辑分离。列表框充当员工数据的视图,Show Claims 按钮处理用户操作(如“单击”)并调用相关的控制器函数。控制器函数是您的 Web 或 Windows 窗体上的一种方法,包含改变业务数据状态和用户界面状态的逻辑。

    将加载窗体之后改变控件状态的授权代码放在这些控制器函数中,而不要放在事件处理程序中。

  • 为每个角色使用单独的窗体 - 如果角色的数量比较少,而且每个角色需要完全不同的窗体,则可以为每个角色创建单独的窗体。测试各个窗体中的角色成员关系,并重定向到正确的窗体中。这也许会增加大量的测试过程,但您可以使用前面介绍的模型视图控制器模式降低由此带来的工作量,并且还能重复使用显示代码。

改变页面流

有时(比如典型向导的用户界面)会根据用户的角色成员关系,要求用户完成多个窗体(或页面)以执行业务流程。这种方法可以改变页面的顺序或“流”。

在上面的申请示例中,GeneralManager 成员可以查看并批准所有部门的员工申请,而 StoreManager 成员只能查看自己部门的员工申请。为了使 GeneralManager 成员可以选择部门,需要另外创建一个步骤来检索该信息,然后再显示员工选择窗体。

以下代码说明了创建这种页面流的方法。这段代码使用 ASP.NET Server.Transfer 方法向不同的页面传递控制。对 Server.Transfer 的调用包含在名为 DisplayNextPage 的私有方法中:

在业务层执行授权

业务过程可以是很简单的业务逻辑,也可以是大型的、运行时间很长的、包含许多信任和非信任伙伴的工作流程。业务层代码必须保证所有的客户请求都能满足组织规定的授权规则和逻辑规则。

您有如下需求时,请在业务层中创建访问检查:

  • 对执行业务流程的操作进行授权。
  • 在调用内部业务组件之前,对来自外部的、非信任源的调用进行授权。为此,需要创建一个 IPrincipal 对象,供授权过程中的业务流程使用。在标识进入业务流程时使用服务接口管理标识,或者对内部业务组件的调用进行授权,或者同时使用这两种方法。
  • 对分布式系统的其他部分或不属于应用程序的外部服务的调用进行授权。为此,需要从组件之外管理标识流。使用服务代理来确保当前用户可以执行外部调用和/或在标识溢出过程之外时可以处理标识。

在业务层创建授权代码时,请使用以下最佳方案:

  • 将授权能力分解到框架和实用程序组件,如自定义的 IPrincipal 对象。这将允许您创建独立于业务逻辑的授权能力。
  • 在高级别过程的开始处执行授权检查。这样可以在较少的位置配置授权,减少维护的工作量。

    但要使解决方案更加安全,需要在每个可以公开访问的方法中执行授权,因为对这些方法的调用可能未通过授权检查。

在 .NET 和 COM+ 中使用基于角色的安全设置对比

COM+ 最有名的功能是可以为应用程序提供企业功能,如事务组件、异步激活和对象合并等。如果您要在 .NET 企业应用程序中使用这些功能,应该使用基于角色的 COM+ 安全设置,而不要使用基于角色的 .NET 安全设置。基于角色的 COM+ 安全系统和基于角色的 .NET 框架安全设置不能互相操作。因此,在设计初期,您就要确定在 COM+ 中驻留的组件和 .NET 类型的组件,否则以后将很难更改授权代码。

有关如何确定基于角色的 .NET 安全系统和基于角色的 COM+ 安全系统的示例,请参阅 MSDN 杂志中的 Unify the Role-Based Security Models for Enterprise and Application Domains with .NET(英文)。

除了基于角色的安全设置以外,COM+ 还内置有传递 Windows 用户上下文和前一调用者信息的能力。

注意:在组件中混合使用基于角色的 COM+ 和 .NET 安全设置是不安全的。因为在执行托管 COM+ 组件时,Windows 会在托管代码和非托管代码之间切换,从而导致重要信息不可靠。

有关基于角色的 COM+ 安全设置代码示例,请参阅附录中的如何使用 System.EnterpriseServices 基于角色的 COM+ 安全设置。有关在 .NET 应用程序中使用 COM+ 的详细信息,请参阅 MSDN Library 中的 Understanding Enterprise Services (COM+) in .NET(英文)。

在业务逻辑组件中执行授权

包含业务逻辑的组件通常都需要执行授权检查。您可以使用 .NET Remoting 或封装业务组件的服务接口(如 XML Web Service)从用户界面层的代码中直接调用这些组件。

您创建的授权逻辑经常会与组件业务逻辑混合,并成为组件业务逻辑的一部分。有关如何分离业务逻辑和授权逻辑的详细信息,请参阅本指南后面的分离业务逻辑和授权逻辑。有关如何创建授权检查的详细信息,请参阅本指南后面的使用基于角色的 .NET 安全设置创建授权代码

在实体中执行授权

实体表示数据,也可能包括一些行为方式。可以使用的实体有许多,如 DataSet、XML 字符串、XML 文档对象模型 (XML DOM),以及应用程序中自定义的实体类,具体使用哪一类取决于应用程序的物理和逻辑设计限制。应用程序既生成实体又使用实体。

实体经常在应用程序层之间移动,包括从数据源检索数据、发送到用户界面再返回到数据源进行更新的整个过程。

您可以创建类来代表执行授权检查的实体。例如,只有属于某一角色的用户才能创建和初始化实体对象。

但是,大多数情况下,不应在实体中执行应用程序授权,因为:

  • 纯数据实现(如 DataSet、XML 字符串和 XML DOM)不提供内置的执行应用程序授权检查的方法。
  • 实体(包括第三方提供的实体)的移动性很高。客户端应用程序不一定能够使用实体授权功能。

在服务接口中执行授权

服务接口是将内部业务逻辑呈现给外部世界的窗口。服务接口向业务流程提供了一个入口点,使您可以灵活地改变内部流程的方法签名,而不影响外部调用者。

非信任方或要求身份验证和授权的一方访问内部业务流程组件之前,服务接口为其提供入口。

在费用申请的示例中,服务接口使用 XML Web Service 允许非信任客户访问申请处理组件,使用 .NET Remoting 或分布式 COM (DCOM) 允许信任客户访问申请处理组件。服务接口也可以是自定义的消息队列 (MSMQ) 侦听器。

有关说明 XML Web Service 中授权的示例代码,请参阅附录中的如何在 XML Web Service 中执行授权

在服务代理中执行授权

服务代理是业务逻辑和外部服务之间的一座桥梁。XML Web Service 和 MSMQ 消息队列就是两个外部服务代理的例子。

可以通过授权从业务层内部调用服务,使用服务代理控制对外部服务的访问。服务代理作为外部服务的代理,在代码调用外部服务时,使您可以执行验证、授权、数据格式化和其他任务。使用服务代理还允许您改变被调用的外部服务及其签名,而不影响您的业务逻辑。可以在授权服务代理中编写代码,防止非授权的用户调用外部服务。

注意:这种授权与在外部服务中进行的授权不同。如果您保持使用外部服务,必须在被调用者内执行授权检查以确保系统的安全。

在下面的服务代理代码中,如果用户不属于 CanApproveClaims 角色,他/她将不能向 ApproveClaim XML Web Service 方法传递 claimXML 消息。

有关处理授权代码异常情况的详细信息,请参阅本指南中的处理授权错误

如果外部服务授权时要求使用特殊的用户标识,请在服务代理中创建代码,以传递可接受的 IIdentity 对象,从而将标识流传递到组件之外。有关管理标识流的详细信息,请参阅本指南前面的设计用于授权的标识流

在数据层执行授权

数据层是最后一道防线,用于抵御来自用户界面、业务组件以及试图直接访问数据的攻击者的非法请求。从授权的角度看,数据层确保只有经过批准的用户才可以访问和修改数据,而不管其访问方式如何。

需要执行以下操作时,请在数据层中创建访问检查:

  • 防止敏感数据泄露到数据层之外。
  • 防止未经授权修改现有数据和插入新数据。
  • 使用存储过程或视图限制对数据的访问,而不是修改对数据表的直接访问权限。

要在数据层执行授权,可以使用多种技术和对象,这取决于您的安全要求。可以在以下数据层组件中执行授权:

  • 数据访问逻辑组件 - 数据访问逻辑组件负责从数据库检索数据、将数据保存回数据库,还负责处理完成这些数据操作所需的逻辑请求。所有应用程序数据都是通过数据访问逻辑组件在数据库和应用程序的其他部分之间传递的。
  • 存储过程 - 存储过程可以在数据库中执行授权逻辑并检索和更新数据。
  • 数据库安全功能 - 使用 SQL Server 内置的安全功能保护数据库对象,例如表、列、视图和存储过程。

有关使用数据访问逻辑组件和实体的详细信息,请参阅 MSDN Library 中的 Designing Data Tier Components and Passing Data Through Tiers(英文)。

以下这些问题能帮助您选择数据层的授权策略:

  • 授权逻辑有多复杂?
    • 简单 - 在存储过程中执行授权检查。
    • 复杂 - 在数据访问逻辑组件中执行授权,并调用不同的存储过程。
  • 存储过程的复杂程度如何?
    • 简单 - 在数据访问逻辑组件中执行授权检查,并调用不同的存储过程(如果创建多个执行相似操作的存储过程不会增加维护难度)。
    • 复杂 - 在存储过程中执行授权检查,该存储过程具有向授权检查提供信息的参数。

      如果创建执行相似操作的多个存储过程会增加维护的难度,则可以向存储过程传递一个值以减少存储过程的数量。

  • 有多少存储过程会涉及授权检查?
    • 较少 - 创建多个涉及授权检查的存储过程版本。创建明显地链接存储过程的命名标准,例如 GetClaimsMGR 和 GetClaimsEMP。但这种方法在应用程序的开发过程中对设计是不利的,因此要慎用。
    • 较多 - 创建多个单独的存储过程,根据连接的用户的不同检索不同的数据。使用表示应用程序角色的服务帐户连接到数据库,允许存储过程执行授权检查。这种方法最大限度地减少了存储过程的数量,因而也减少了维护的工作量。

在数据层创建授权代码时,请使用以下最佳方案:

  • 连接到服务帐户较少的数据库,以使连接池的性能达到最优。当连接信息(如用户信息)与现有连接匹配时,连接池允许应用程序重复使用连接。因此,不必为每个用户创建连接。

    有关连接管理的详细信息,请参阅 MSDN Library 的 .NET Data Access Architecture Guide(英文)中的“Managing Database Connections”。

  • GRANTDENY 权限保护数据库对象。

    拒绝对数据表的直接访问可以使用应用程序(如查询分析器)防止连接到数据库的用户,而不会强制对数据的业务逻辑进行更新或查询。只有允许的用户/服务帐户可以访问存储过程和视图。

  • 在使用包括 WHERE 子句的应用程序授权技术时,始终使用存储过程。这样有助于防止 SQL 夹带攻击,实施这类攻击的攻击者会将其他的(通常是恶意的)SQL 语句附加到合法的 WHERE 子句中。有关 SQL 夹带攻击的详细信息,请参阅 MSDN Library 中的 Building Secure ASP.NET Applications(英文)。

在数据访问逻辑组件中执行授权

数据访问逻辑组件是在访问数据库之前最后一个提供功能的组件。因此,您可以利用它们来确保只有获得授权的用户才可以访问或修改数据。

出现以下情形时,请在数据访问逻辑组件中执行授权:

  • 多个应用程序使用相同的数据访问逻辑组件。
  • 需要对数据访问逻辑组件或数据存储库提供的强大功能的访问进行保护。
  • 需要限制用户对敏感数据的访问。

在数据访问逻辑组件中使用基于角色的安全设置的例子包括:在数据传递到其他应用程序层之前对数据进行过滤、控制代码执行的存储过程。

有关如何执行授权检查的详细信息,请参阅本指南后面的执行授权检查

在数据访问逻辑组件中过滤数据

为了保证只向其他应用程序层返回适当的信息,应该在数据访问逻辑组件代码中有选择地删除敏感数据。

下面的代码示例在用户不属于 Manager 角色时,删除数据的 ConfidentialNotes 列。

从数据库中检索不需要的列是对资源的一种浪费,尤其是不需要的列很多时。相反,使用存储过程只选择必要的数据,可以改善数据库引擎的性能。

从数据访问逻辑组件调用不同的存储过程

您可以创建仅为一个角色返回正确数据的多个存储过程。然后,数据访问逻辑组件代码执行授权检查,根据用户角色确定要使用哪些存储过程。

例如,可以为 Manager 创建一个存储过程版本,为 Employee 创建另一个不同的版本,两者只是检索的数据不同。数据访问逻辑组件代码将检查用户的角色成员关系,并调用正确的存储过程。

以下代码显示了数据访问逻辑组件方法如何根据用户的角色成员关系选择要调用的存储过程。这些代码使用了一个辅助类,以避免显示过多的 ADO.NET 代码。Microsoft Application Blocks for .NET(英文)提供了 SqlHelper 类:

在存储过程中执行授权

在存储过程中执行授权的方法主要有两种:

  • 传递角色标志 - 在数据访问逻辑组件中执行授权检查,将授权结果标志传递到存储过程。这样将允许存储过程根据授权标志过滤数据,如以下代码所示。
  • 传递用户标识或其他角色信息 - 数据访问逻辑组件将用户标识作为参数传递到存储过程。然后,存储过程使用该参数来查找用户的角色成员关系并选择数据,如以下代码所示。

在数据库中执行授权

以数据库安全功能作为安全解决方案的基础。然后在数据库安全功能之上使用存储过程设计应用程序授权,如本指南前面所述。

使用数据库安全功能执行授权:

  • 使用 SQL 数据定义语言 (DDL) 安全语句,如 GRANTDENYREVOKE,控制对表、列、视图、存储过程和其他数据对象的访问。
  • 以可能的最低级别(只有数据)限制对敏感数据的访问。SQL Server 本身不支持低级别授权,但是,您可以通过创建视图和存储过程来模拟低级别授权,本主题的后面将介绍有关内容。
  • 尽可能地加强安全性。注意,较高的安全性会降低可伸缩性。

在数据库中执行授权之前,必须选择应用程序连接到数据库的方式。

使用 SQL 角色简化管理

设计系统时,必须考虑使用 SQL Server 角色简化对 SQL Server 的维护。SQL Server 提供了多种角色,包括:

  • 用户定义的数据库角色 - 由数据库唯一的 sp_addrole 存储过程创建的角色。然后可以使用 sp_addrolemember 存储过程向这些角色添加 SQL 用户。
  • 应用程序角色 - 为应用程序而不是为 Windows 用户或组创建的角色。
  • 固定的数据库角色 - 链接到通用数据库功能的固定角色,例如 SQL 管理员和数据库创建者:sysadmindbcreator

有关 SQL Server 安全性、授权和授权角色的详细信息,请参阅 MSDN Library 中的 Building Secure ASP.NET Applications(英文)。

考虑将应用程序角色链接到 SQL Server 角色,使应用程序和 SQL Server 的管理一致。将直接映射到 SQL 角色的服务帐户连接到数据库,以实现这种链接。下面一节介绍了这种技术。

连接到数据库

为了控制对表、列、视图、存储过程和其他数据对象的访问,SQL Server 必许能够识别需要授权的用户,这种识别可以通过以下连接方法实现:

  • Windows 身份验证 - 如果将 SQL Server 配置为只使用 Windows 身份验证,则必须在用户上下文中执行建立连接的过程。SQL Server 使用该 Windows 用户帐户进行所有形式的授权。

    这种方法提供的连接最安全,因为它不通过线路传递凭据。

  • SQL Server 身份验证(混合安全设置)- 如果将 SQL Server 配置为使用 SQL Server 和 Windows 身份验证,则既可以使用 Windows 身份验证,也可以使用用户名和密码在连接字符串中指定用户来进行身份验证。

无论以何种方式连接到数据库,都要先创建几个代表应用程序角色的服务帐户,然后根据角色要求,为各个服务帐户授予权限。

例如,创建 USR_MANAGER 和 USR_APPROVER 等服务帐户以实现最少特权的原则,即只允许访问一部分应用程序功能。

这种方法会带来某些不便,因为当您使用 Windows 身份验证连接到数据库时,需要从数据访问逻辑组件模拟服务帐户。

有关管理数据库连接的详细信息,请参阅 MSDN Library 中的 .NET Data Access Architecture Guide(英文)。

控制对敏感列的访问

使用 T-SQL 语句或 SQL Server 企业管理器实用程序,在列级别应用 GRANTDENYREVOKE 权限,从而实现对列的访问控制。请尽量少用这种授权技术,因为如果数据库很大,要维护的列权限列表会很长,在实际操作中非常耗时。如果您要使用这种技术,请在连接到数据库时使用服务帐户,以使维护工作易于管理。

控制对敏感行的访问

SQL Server 没有隐含地让您控制对各行的访问。但是,您可以向数据表添加安全列,创建控制行的功能。安全列可以保存单独的用户名或访问数据需要的最低角色(假设角色的组织形式是分层的)。安全列允许您限制对各行的访问,但是如果用户可以直接访问数据表,该技术将没有效果。

可以结合使用视图或存储过程中数据表的安全列功能和 SUSER_SNAME 函数,控制对各行的访问。SUSER_SNAME 函数将检索连接的用户名(很可能是服务帐户名)。

以下代码显示如何创建一个视图,该视图只返回当前用户的行。

注意:该方法要求您在连接数据库时使用单独的用户标识,以便 SUSER_SNAME 函数能够检索到正确的信息。在这种情况下,应用程序将无法利用连接池的优势。

使用基于角色的 .NET 安全设置创建授权代码

本节介绍如何使用基于角色的 .NET 安全设置来编写执行授权的代码。基于角色的安全设置可使您对整个用户类别或用户集执行授权,而不只是对特定用户执行授权。

您必须考虑以下问题:

  • 执行授权检查 - 要使用基于角色的 .NET 安全设置执行授权检查,有多种编程技术可用,而您必须根据自己的应用程序要求确定具体使用哪种技术。
  • 将业务逻辑和授权逻辑分开 - 需要具备将业务逻辑和授权逻辑分开的策略,这一点很重要。
  • 处理授权错误 - 您必须确定处理授权错误的方法。授权错误不仅会影响您的组件代码,而且还会影响那些要调用该组件的代码。
  • 使用多线程执行授权 - 如果您的应用程序使用了多线程技术,就必须确定如何实现 IPrincipal 对象与各个线程的最佳组合。解决此问题有多种方法可供选择,具体的使用方式取决于您如何在多线程应用程序中处理基于角色的授权。
  • 扩展默认实现 - 有些情况下,对 .NET Framework 类库中的类和接口提供的默认实现进行扩展是非常有益的。

执行授权检查

.NET Framework 提供了三种方法来检查某个主体是否是角色的成员:

  • 手动检查
  • 命令式检查
  • 声明式检查

本主题将为您逐一介绍这些技术,并对其各自的使用场合提供指导。上述三种技术提供的性能相同,因为它们都使用执行线程的主体的 IsInRole 方法。同样,这三种技术还都假设主体已自动或手动链接到 Thread.CurrentPrincipal 属性,如本指南前面的设计用于授权的身份验证中所述。

但是,在检查某个主体是否为角色的成员这个问题上,这些技术之间存在一些明显的差别。例如:

  • 手动和命令式检查都允许包括变量的复杂检查,这在后面会介绍。选择手动检查还是命令式检查完全取决于个人喜好。
  • 当检查只是涉及简单地比较常量角色值而不包括变量或复杂逻辑时,使用声明式检查比较合适。

在这三种方式中,手动检查角色是最常见的方法。

手动检查角色

可以通过调用 IPrincipal 对象的 IsInRole 方法,手动检查某个用户是否属于某个角色。但是,GenericPrincipalsWindowsPrincipalsIsInRole 是不一样的。

使用 GenericPrincipals 检查角色

当手动使用 GenericPrincipals 检查角色时,必须使用一个字符串来表示角色名。例如,以下代码将检查用户是否属于 Claims Approver 角色。

使用 WindowsPrincipals 检查角色

当使用 WindowsPrincipals 手动检查角色时,有两种不同的方法表示角色名:

  • 字符串 - 如果您正在检查系统管理员创建的自定义 Windows 用户,如“<域>\Claims Department”或“<机器名>\Claims Department”,请使用字符串。例如,以下代码将检查用户是否属于名为“<域>\Claims Department”的自定义 Windows 用户组:

    如果是本地计算机级别的组,请使用 Environment.MachineName 属性。当您向其他的机器部署该属性时,无需改变代码。以下代码说明了 Environment.MachineName 属性的用法。

    注意:当您使用字符串值检查内置的本地 Windows 用户组(如 Administrator 或 User)时,必须指定单词 BUILTIN(如 BUILTIN\<组>),说明这是特殊的用户组名。

    使用 Whoami.exe 工具查看用户的 Windows 用户组的正确语法。Whoami.exe 工具可以从 http://www.microsoft.com/windows2000/techinfo/reskit/tools/existing/whoami-o.asp(英文)获得。要显示用户角色的完整列表,请在执行工具时指定 /all 开关。

    注意:基于角色的 .NET 安全设置不支持 Windows 2000 格式“username@domain.com”。
  • WindowsBuiltInRole 枚举类型 - 使用 WindowsBuiltInRole 枚举类型检查角色成员关系是否属于内置的 Windows 用户组,如 Administrator 或 User。例如,以下代码将检查用户是否属于内置的 Windows User 角色。

    这种枚举可以简化本地化代码的过程,因为 Windows 安装环境不会影响用户组列表。

实现复杂的手动检查

不管您使用的是 GenericPrincipal 还是 WindowsPrincipal,都可以在 C# 语言中通过使用包含逻辑运算符(如逻辑 AND [&&] 和逻辑 OR [||])的多段 IF 语句执行复杂的访问检查。以下代码显示了这样的检查:

您可以通过组合运算符,如小于号 (<) 和大于号 (>),来执行复杂的检查。

通过抛出异常来处理授权故障。有关传递授权检查故障的详细信息,请参阅本指南后面的抛出授权异常

命令式检查角色

您可以使用命令式检查来强制某一用户属于某种特定的角色。通过创建 PrincipalPermission 对象并指定所需的角色,对用户的角色成员关系进行命令式检查。然后由 Demand 方法执行访问检查。如果主体不是命令角色的成员,则公共语言运库将生成一个 SecurityException

以下代码检查活动的主体是否属于应用程序指定的角色 CEO。

注意:本主题中的所有示例都向 User 属性的 PrincipalPermission 构造函数传递了一个 null 值,以使属于相应角色的用户通过访问检查。如果您指定了用户名,运行时将检查特定的用户,这将影响使用角色的目的。

您可以使用以下技术对角色成员关系进行命令式检查:

  • 实现 OR 方案 - 可以使用代表 OR 方案的 Union 方法执行复杂检查。以下代码将检查用户是否是 CEO 或 Senior Manager 角色的成员。
  • 创建 AND 方案 - 通过顺序执行两种检查,创建 AND 方案,如以下代码所示。
  • 使用变量 - 您可以综合使用变量、域名和机器名,动态检查角色名。这与执行手动检查的方法相似,如本主题前面所述。以下代码将检查用户是否是 Claims Department 角色的成员,而 Environment.UserDomainName 属性获取与当前用户相关的网络域名。
注意:PrincipalPermission 类也有一个 Intersect 方法,但是该方法不用于角色检查,因为两个用户永远不会相交。该方法仍存在的原因是实现 IPermission 接口时要用到它。

您可以使用标准的异常处理技术来处理调用 Demand 失败时生成的异常。有关传播授权检查失败的详细信息,请参阅本节的处理授权错误

声明式检查角色

您可以将 PrincipalPermissionAttribute 放到类、方法和属性上,公开要求角色权限,如以下代码所示。请注意,成员级属性会覆盖所有类级属性。

在编译过程中,.NET 编译器将 PrincipalPermissionAttribute 保存为程序集元数据中的 XML。在应用程序执行过程中,运行时实例化 PrincipalPermission 对象并执行访问检查。因此,命令式和声明式的访问检查的功能类似。

在通过授权检查之前,运行时不会为调用创建代理,也不会封送任何参数,因此在授权检查失败时性能会有所提高。如果检查失败,运行时将自动抛出 System.Security.SecurityException,受保护的方法代码均不能执行。由于方法均不能执行,所以不能根据业务逻辑执行更复杂的授权,例如比较参数值。

在编译时,运行时将计算属性的值,因此您必须在元数据中对角色硬编码,这样就不能使用诸如 Environment.MachineName 之类的值实现动态解决方案。您也不能使用 WindowsBuiltInRole 枚举,因为声明式检查使用的是 IPrincipal 基本实现。

如果指定多个 PrincipalPermissionAttribute,则这些属性将组合起来形成 OR 方案。因此,只要主体是某一角色的成员,授权检查就能成功。以下代码要求该方法的调用者属于 CEO 或 Senior Manager 角色:

注意:不能使用声明式方法检查用户是否属于某个角色列表。

使用声明技术有助于将授权代码与其他业务逻辑分开。将业务逻辑和授权要求分开可以改善代码的可维护性。做出这种分离之后,改变授权逻辑时不用改变业务逻辑,从而降低了由于疏忽而在代码中引入业务逻辑错误的可能性。

分离业务逻辑和授权逻辑

基于角色的 .NET 安全框架的设计人员对 IIdentityIPrincipal 接口进行了简单的定义,目的是使开发者可以对接口进行扩展以满足自己的需要。出现以下情形时,可能需要扩展该功能:

  • 需要保存应用程序要求的有关用户的一些额外信息(不只是用户名和角色)。

    例如,有可能需要保存组织地址代码等信息,以说明用户是否位于特定的地点。

  • 需要使用您自己的基于角色的安全信息源。

    .NET Framework 提供了一种从 Windows 检索角色信息的实现(使用 WindowsIdentityWindowsPrincipal 类)。如果您要维护自己的信息存储库,通常需要实现自定义的扩展。

  • 对授权有特殊要求。例如,可能需要编写与部署位置相关的代码,这是为实现授权检查而需要“外部知识”的一个示例。

处理授权错误

如果业务过程中的授权检查失败,组件代码需要通知失败的调用者。最好的通知方法是抛出一个异常。如果您正在实现一个异步解决方案,那么处理这种异常只要采取处理标准异步异常的方法即可,例如在使用 MSMQ 时向异常队列中插入一个异常。

在您设计组件时,假定在信任边界的“看门人”处已进行了相应的授权检查。如果违反了这种假设,组件应抛出异常。例如,您可以假设用户界面不允许非 Administrator 角色的用户删除文件。应用程序创建用户界面时,会在入口处实施有效的检查,而执行删除文件逻辑的组件必须遵循这种假设。

抛出授权异常

当应用程序授权检查失败时,可以抛出以下类型的异常:

  • System.ApplicationException - 抛出 ApplicationException,用一条消息提示应用程序授权检查失败。如果您将授权检查作为业务逻辑的子集,可以在应用程序中使用这种异常类型。例如:
  • System.Security.SecurityException - 抛出 SecurityException,用一条消息提示应用程序授权检查失败。例如:

    当命令式或声明式授权检查失败时,运行时会自动抛出这种异常。

    如果您认为授权失败会导致安全违例,可以在应用程序中使用这种异常。这种异常类型可用于捕捉某个 catch 处理程序中的所有与安全相关的异常。

    代码访问安全性和其他系统级的操作也会抛出该异常,这会增加调用者过滤异常和采取相应措施的难度。

  • 自定义异常 - 如前所述,使用声明式和命令式授权模式时,SecurityException 是默认的异常类型。运行时返回标准信息:“请求主体权限失败”。您可能需要根据特定的错误添加额外的信息,帮助调用者以特定的方式处理异常。为此,您可以定义自己的自定义异常类,来表示应用程序中发生的不同类型的授权错误。

    您可以按照以下方法,创建有益于应用程序的自定义异常类:

    • 调用代码为各个异常类型定义单独的 catch 处理程序,以特定方式处理错误。例如,调用代码可以过滤某一类型的异常,并为实现操作而记录它们。
    • 如果授权异常类设置正确,您可以在涉及授权检查的多个应用程序中重复使用这些类。
    • 可以将常见的授权故障类型组合在一起,使相互聚合的过程和组件能够执行正确的操作。

    有关创建自己的异常及其实现示例的详细信息,请参阅附录中的如何创建授权自定义异常类型

注意:传播的授权异常不应包含任何可能损坏系统的敏感信息。有关这方面的信息和其他异常管理最佳方案的详细信息,请参阅 MSDN Library 中的 Exception Management in .NET(英文)。

处理授权异常

除了基于角色的授权异常以外,调用代码还可以为授权异常定义 catch 处理程序。采用的部署方案不同,您收到的 SecurityException 违例的形式可能也不同。例如,如果您从半信任环境启动代码,运行时可能抛出异常,表示代码访问安全性权限失败(如文件访问问题)。尽管这些异常与基于角色的授权不相关,但是可能导致 SecurityException

可以编写异常处理代码来区分基于角色的授权异常和其他类型的授权异常。使用 SecurityException 对象的 PermissionType 属性来检查异常是否与基于角色的授权相关,如以下代码所示:

PermissionType 属性的结果允许您过滤代码访问安全性和基于角色的授权异常。过滤异常之后,您可以抛出自定义的异常或在被调用者内部处理异常。

使用多线程执行授权

大多数基于 .NET 的应用程序都使用多线程以改进响应性或异步地执行工作。使用主体时,多线程会使问题变得复杂,因为每个线程都必须链接到一个 IPrincipal 对象。运行时将 IPrincipal 对象复制到父线程创建的所有新线程中。

根据以下规则,Thread.CurrentPrincipal 属性返回一个不同的 IPrincipal 对象:

  • 如果明确地将一个 IPrincipal 对象与线程关联,将返回该 IPrincipal 对象。
  • 如果没有明确地将 IPrincipal 对象与线程关联,并使用了 SetThreadPrincipal 方法,将返回应用程序域的默认 IPrincipal 对象。

    调用 AppDomain.CurrentDomain.SetThreadPrincipal 方法,可以保证运行时为当前的和新的线程使用特定的 IPrincipal 对象。对于 ASP.NET,建议您不要使用此选项,因为主体由运行时处理,而且此选项还会影响由其他用户运行的其他线程。

    注意:如果在应用程序域的有效期内多次调用此方法,运行时将抛出一个 PolicyException
  • 如果没有 IPrincipal 对象或默认应用程序域 IPrincipal 对象与线程关联,将返回一个在 SetPrincipalPolicy 方法中指定的 IPrincipal 类型对象,如前所述。
  • 如果您没有使用以上方法,将返回一个 null(或 Nothing)。

扩展默认实现

当扩展基于 .NET 的应用程序授权框架时,请遵循以下原则:

  • 实现 IIdentity 和 IPrincipal 接口 - 这将确保您的扩展与使用基于角色的 .NET 安全模型的其他功能(例如 ASP.NET URL 授权功能)兼容。
  • 尽可能使用延迟初始化 - 如果为构造 IPrincipal 对象而导入角色信息的任务过于耗时,应该推迟此活动,等到第一次需要信息的时候再进行。
  • 将 IPrincipal 对象附加到执行线程 - 这将允许您使用前面提到的声明式、命令式和手动授权检查,而且还可以与其他技术(如 ASP.NET 授权)较好地协同工作。

决定扩展策略

决定要创建的扩展类型以及在什么位置创建。表 1 包含一些常见的扩展,并且说明应该在哪里创建需要的附加逻辑。

表 1:创建应用程序授权扩展的位置

扩展类型 自定义标识 自定义主体 Helper 类
用户指定的信息 X    
相关的权限或角色   X  
需要的外部知识     X

实现自定义标识

GenericIdentityWindowsIdentity 对象提供的功能不能满足您的需要时,您应该创建自己的类来实现 IIdentity。由此可能产生的增强功能包括:

  • 管理令牌或用户标识符。当需要封装涉及令牌的身份验证过程时,一般需要使用此方法。WindowsIdentityFormsIdentityPassportIdentity 类管理代表用户的令牌。这些类分别使用 Microsoft Win32® 令牌、窗体身份验证票据和 Passport 唯一标识符 (PUID)。

    例如,使用 ISAPI 单一登录解决方案时,您可以创建自己的自定义标识。

  • 将您自己的代码封装在 IIdentity 对象中,然后将其提供给所有应用程序使用。例如,PassportIdentity 通过标准的 IIdentity 接口提供许多额外属性和方法,包括配置信息。可以使用标准技术(如公共属性)将这些额外信息添加到您的标识类中。另一个例子是经常需要根据用户的经理进行授权检查,而在本解决方案中,您可以使用标识值提供用户的经理。

实现自定义主体

GenericPrincipalWindowsPrincipal 对象提供的功能不能满足您的需要时,您应该创建自己的类来实现 IPrincipal。由此可能产生的增强功能包括:

  • 实现您自己的角色枚举类型,方法与 WindowsPrincipal 中使用的 WindowsBuiltInRole 枚举方法相同。实现此功能的方法是重载 IsInRole 方法,以供调用代码选择是否使用您的枚举类型或字符串。
  • 检查主体是否属于指定列表中的某一角色。例如:
  • 访问列表或枚举主体所属的角色。例如:
  • 强制划分某些类型的角色层次结构,以使某些角色“大于”或“小于”其他角色。例如,Senior Manager“大于”或高于 Junior Manager。

重复使用授权实现

如果在设计框架时考虑到重复使用,您就可以在多个应用程序中重复使用授权框架。可以重复使用的内容一般包括用于实现授权能力的代码和结构。

重复使用应用程序授权框架的好处有:

  • 组织内的开发人员可以将一个应用程序授权框架作为工作目标。
  • 可以很容易地在其他应用程序中重复使用支持应用程序授权框架的组件,因为这些应用程序使用了标准的授权方法。
  • 授权子系统成为与其他系统或平台同步的中心。重复使用减少了旧应用程序与需要授权数据的新应用程序之间同步授权详细信息的需求。
  • 授权管理在多个应用程序中是一致的,因而管理和操作人员能够进行相同的工作。

本节介绍重复使用授权实现的一些最佳方案。

创建可重复使用的授权框架

您可以选择要在应用程序中重复使用的应用程序授权框架数量。选项有:

  • 重复使用一个授权数据库和一个访问授权数据的 XML Web Service。
  • 重复使用授权框架,但是每个应用程序使用独立的数据库实例。

图 6 显示了实现应用程序授权重复使用的示例。

图 6:应用程序授权重复使用的逻辑表示

请注意图 6 中的以下几点:

  • 不同的应用程序可以重复使用相同的授权框架。
  • 授权服务提供对授权数据的安全访问。
  • 中央数据库保存授权数据。
  • 一个管理实用程序提供所有的配置。
  • 本地缓存改进授权性能。

表 2 描述了图 6 中显示的各个组件,并解释了在不同项目和不同应用程序中重复使用这些组件的方法。

表 2:如何重复使用授权组件

组件 说明 重复使用的方法
授权框架 创建的自定义代码,用于支持授权能力,如自定义主体、标识和异常。 创建包含所有自定义的、可重复使用的代码的程序集,以便用于其他工程。
还可以为程序集赋予严格名称,并将程序集安装到全局程序集缓存中。
授权缓存 一种可选的保存授权信息的单元,距离使用它的框架所处的实际位置很近。 使用适当的方法创建 Hashtable 对象(或内存存储库中的类似对象),按照需要进行刷新。为了提高效率,IPrincipal 对象应使用授权缓存检索角色成员关系,而不是进入数据库。
如果需要保护或隐藏缓存中的数据,则使用加密;否则,就对缓存中的数据进行签名以防止数据被篡改(如果数据保存在本地或其他易受攻击的位置,这一点就尤其重要)。
授权服务 到授权数据的接口(或“看门人”) 创建一个进程外组件,在可以访问授权存储的安全环境中运行。进程外组件可用于授权对存储库的访问,方法是检查访问是否是由指定帐户进行的,从而防止其他用户访问存储库。
另外,您也可以构建一个集中复用的 XML Web Service,这样提供的互操作性最高。
请记住保护授权服务和授权框架之间的通信通道。
管理实用程序 应用程序管理员用来维护授权数据的用户界面。 创建一个管理员可以调整授权设置的中央网站,
也可以创建一个可重复使用的图形用户界面,部署在各个应用程序中。
授权数据 授权数据方案、存储过程和数据访问逻辑组件的定义 对所有授权数据使用一个数据库。
另外,所有的应用程序都使用标准的授权方案。

保证实现的安全性

为了保证可重复使用的授权框架的安全性,请遵循以下原则:

  • 对授权数据存储库的调用者执行身份验证检查,防止非授权方查询授权信息。
  • 使用安全传输,例如 SSL 或 IPSec。
  • 在各个应用程序的单独目录中部署授权服务,如 XML Web Service,这样可以配置服务,使其仅在部署它的应用程序中发挥作用。

改进可重复使用的授权框架的性能

使用以下技术改进可重复使用的应用程序授权框架的性能:

  • 尽可能使用成批授权查询,以避免频繁的进程外操作。例如,用一个请求为多个用户检索角色。
  • 将授权数据缓存到距离内存存储库(如 Hashtable)很近的位置。缓存还可以降低对底层存储库的位置和组织的依赖性。为了改善性能和增强安全性,您还应该为每台物理计算机都设置单独的缓存。
  • 实现缓存信息的预定刷新或按需刷新。
  • 实现授权缓存的延迟初始化,避免在未进行访问检查时检索授权信息。

附录

本附录包括以下主题:

如何在 .NET Remoting 组件中启用授权

.NET Remoting 组件不会自动将主体从一个应用程序域传递到另一个应用程序域,除非这两个域在同一过程中。您必须手动在调用者和被调用者之间传递此信息,并保证在传递过程中信息处于安全状态。

有关使用 Windows 身份验证的 .NET Remoting 的详细信息,请参阅 MSDN Library 中的 .NET Remoting Security Solution(英文)。

在使用非 Windows 身份验证时,可以使用 CallContext 类和 ILogicalThreadAffinative 接口提供标识流。

当远程对象调用方法时,CallContext 类允许包含附加信息。该信息可以是单个方法要求的任意信息,例如用户信息。

只有实现 ILogicalThreadAffinative 接口的类型才被发送到单独应用程序域中的服务器。例如,您可以创建一个实现 ILogicalThreadAffinative 的简单类型,存储基于 IIdentityIPrincipal 的对象。远程对象可以访问此对象,并且您可以检查角色的成员关系。

以下代码显示了如何创建实现 ILogicalThreadAffinative 接口和包含基于 IPrincipal 的对象的类。

以下代码显示了远程对象如何使用 CallContext.GetData 方法,通过 PrincipalStorage 类来检索 IPrincipal 信息。

以下代码显示了客户端应用程序如何使用 CallContext.SetData 方法在调用远程对象之前设置 IPrincipal 信息。

以下代码显示了注册远程对象的服务器应用程序。

如何在 XML Web Service 中执行授权

在下面的代码中,ProcessClaim XML Web Service 方法执行授权检查,并在检查失败时抛出异常。检查将确定费用申请的发出者是否被授权发送申请。

为了确保正在处理的被调用者已通过身份验证,需要指定 PrincipalPermissionAttributeAuthenticated 属性。这样做还可以确保 Thread.CurrentPrincipal 自动为命令式授权检查提供正确的 IPrincipal 对象。

以下代码将调用 ProcessClaim 服务。只有 Employee 角色中的用户才能向 XML Web Service 提交员工申请。

这段代码假定已通过验证的用户属于应用程序定义的角色,可以是 Employee 或 Contractor。有关如何加载具有角色的 IPrincipal 对象的详细信息,请参阅附录中后面的如何在 ASP.NET 应用程序中更改主体如何使用 SQL Server 构建 GenericPrincipal

如何创建授权自定义异常类型

为了解决问题或进行审核,有可能需要抛出自定义异常,其中包含典型的授权失败信息和其他信息(例如引起异常的人的用户名或电子邮件地址)。以下代码将创建一个异常类,以处理授权失败。该异常类包括来自 Thread.CurrentPrincipal.Identity 的用户名。

以下代码将调用执行授权检查的组件。如果检查失败,catch 块将测试异常,并通过 ApplicationAuthorizationException 显示用户名。

以下代码将作为执行授权检查的被调用者组件。这段代码已特意设计为拒绝访问特定的文件,以便出现与授权无关的异常。catch 块使用 PermissionType 属性过滤两种可能的异常。

如何在 ASP.NET 应用程序中更改主体

您可以更改 ASP.NET 应用程序中的默认 IPrincipal 对象,使其包括在身份验证发生之后从数据存储库加载的特定于应用程序的角色。更改 IPrincipal 对象之后,您可以继续使用应用程序定义的角色进行授权检查,同时仍然可以依赖任意类型的授权。最好在 Application_AuthenticateRequest 事件处理程序中更改 IPrincipal 对象,因为可以使用通过验证的标识,而且事件处理程序是在网页或 XML Web Service 逻辑之前执行的。

以下代码根据通过验证的标识用 GenericPrincipal 对象代替 HttpContext.CurrentIPrincipal 对象,并向 HttpContext.Cache 添加对象以改进性能:

该示例使用缓存来减少查询数据库的次数。示例中之所以选择缓存是因为缓存允许您设置超时值。但是,如果使用会话状态,则可以减少测试过程,因为 ASP.NET 可以保证用户只访问自己的会话。使用此类代码时必须小心,确保不要用错误的主体引发运行用户请求的线程。有关 GetPrincipal 方法的实现示例,请参阅下一主题。

如何使用 SQL Server 构建 GenericPrincipal

以下示例显示了一个简单的数据库方案,可以用来导入带有角色的 GenericPrincipal。运行以下代码创建 UserRoles 表:

其后的 GetPrincipal 方法带有 IIdentity 参数,向 GenericPrincipal 提供标识信息。该代码使用标识名在 UserRoles 表中查找角色,以便将角色导入 IPrincipal 对象。

请注意,该代码将标识名作为字符串传递到 GenericIdentity 构造函数,而没有使用 IIdentity 参数进行传递。将标识名作为字符串传递,使得代码可以与任意类型的身份验证结合使用。

代码还向 GenericIdentity 构造函数传递了另一个字符串,表示最初使用的身份验证类型。

有关调用 GetPrincipal 方法的代码示例,请参阅附录中前面的如何在 ASP.NET 应用程序中更改 Principal

如何使用 System.EnterpriseServices 基于角色的 COM+ 安全设置

.NET 应用程序使用 COM+ 来提供基于角色的安全设置时,可以使用以下两种方法之一检查角色成员关系:手动检查或声明式检查。

执行手动检查或声明式访问检查

  1. 在代码中,在程序集级指定 ApplicationAccessControlAttribute,使用 AccessChecksLevel 属性设置授权级别。

    使用 AccessChecksLevelOption.ApplicationComponent 值执行应用程序级和组件级检查。

  2. 对相应的类应用 ComponentAccessControlAttribute,对需要基于角色的安全设置的组件启用访问检查。
  3. ServicedComponent 中派生出所有要求基于角色检查的类。
  4. 在程序集、类、接口或方法级使用 SecurityRoleAttribute,为应用程序添加需要的角色。
注意:步骤 1、3 和 4 实际上并不需要在代码中实现,它们可以在组件服务管理控制台中完成。

完成组件服务应用程序的配置

  1. 至少要在调用级配置身份验证,否则,拦截者无法访问调用者的信息。
  2. 为用户分配相应的角色。
注意:要使应用程序能够正常工作,您可能还需要增加几个步骤,如在全局程序集缓存中安装程序集。这些步骤与应用程序部署相关,本指南不作介绍。有关安装组件服务应用程序的详细信息,请参阅 MSDN Magazine 中的 COM+ Integration: How .NET Enterprise Services Can Help You Build Distributed Applications(英文)。

执行手动 COM+ 角色检查

手动角色检查依赖于安全性是否可以调用应用程序代码中的上下文对象。如果启用组件服务应用程序的安全性,则上下文对象可用。

手动检查 COM+ 角色成员关系时,请使用以下技术:

  • 调用 SecurityCallContext 对象的 IsCallerInRole 方法,检查用户是否属于某个特定角色。作为预防措施,核查是否在检查角色成员关系之前启用了安全设置。如果未启用安全设置,则调用 IsCallerInRole 的结果始终是 true

    以下代码显示了如何测试安全性并检查角色成员关系。

  • 通过使用包含标准语法(如逻辑 AND [&&] 和逻辑 OR [||] 检查)的多段 IF 语句创建复杂的检查。以下代码就是这样的一个测试语句。
  • 在应用程序中创建常数,或从外部源(如数据库)中检索用户角色。
  • 使用 SecurityCallContext.CurrentCall.OriginalCaller 属性访问用户信息。该属性返回一个 SecurityIdentity 对象,如以下代码所示。

执行声明式 COM+ 角色检查

声明式检查启用属于特定角色的用户,以激活组件并调用其任一公共方法。这种类型的角色检查与在类级指定基于角色的声明式 .NET 安全属性相类似。在类级使用 SecurityRoleAttribute 指定可以访问类的角色,如以下示例所示。

执行声明式检查直到方法级

对方法执行声明式检查需要的步骤比类级检查多:

  1. 实现接口。
  2. 在类级或方法级包含 SecureMethodAttribute。当您将应用程序安装到 COM+ 中时,此属性负责在方法级配置角色安全设置。要使所有方法都可以在代码中声明受到保护或者是手动使用管理实用程序来保护所有方法,请在类级指定此属性。
  3. 要将方法链接到角色,请在方法实现中使用 SecurityRoleAttribute
  4. 在组件服务管理控制台中向 Marshaler 角色手动添加用户。当将应用程序安装到 COM+ 中时,COM+ 将自动创建该角色,以便托管客户端和非托管客户端都可以调用安全的方法。

以下代码演示了这些步骤。

反馈和支持

合作者

衷心感谢以下各位参与撰稿和校对的专家:Erik Olson、Dave McPherson、Riyaz Pishori、Srinath Vasireddy、Abhishek Bhattacharya (Sapient)、Dimitris Georgakopoulos (Sapient)、Chris Brooks、Ross Cockburn、Kenny Jones、Angela Crocker、Andy Olson、Lance Hendrix、Mark White、Steve Busby、Unmesh Redkar、J.D. Meier 和 Diego Gonzalez。

posted on 2005-09-12 09:16  WIND  阅读(2666)  评论(0编辑  收藏  举报

导航

ECubeCMS