本指南介绍为基于 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 的应用程序中实现授权,解释术语“授权”并讨论几种执行授权的机制。本指南还包括以下内容:
采用何种授权机制通常取决于验证用户身份(标识)的方法。本指南将探讨以下内容:
在典型的企业应用程序中,需要在应用程序的不同层次执行不同类型的授权。为了帮助您识别各层的授权需要以及在不同的方案中选择合适的授权策略,本指南介绍在用户界面层、业务层和数据层中通常使用的典型授权任务。图 1 显示了企业应用程序的各个层上出现的一些重要授权问题。
图 1:在企业应用程序的各层中执行授权
.NET Framework 类库提供了多种接口和类,帮助您使用基于角色的 .NET 安全设置来执行授权。本指南介绍:
定义授权框架时所做的大部分工作,都可以在多个应用程序中重复使用。本指南将对以下内容进行总结:
注意:本指南适用于使用 .NET Framework 功能进行由应用程序管理的授权。Microsoft Windows® Server 2003 系列操作系统中的授权管理器 API 和 Microsoft Management Console (MMC) 管理单元,为应用程序提供了具有完整的基于角色的访问控制框架。授权管理器 API 也称为 AzMan,它提供了一种简化的开发模型,用于管理灵活的分组和业务规则,并可用于存储授权策略。有关详细信息,请参阅 MSDN Library 中的 Authorization Interfaces(英文)和 Authorization Objects(英文)。
“授权”是对通过验证的主体(用户、计算机、网络设备或程序集)是否具有执行某项操作的权限的确认。授权提供的保护只允许指定用户执行特定操作,并防止恶意行为。
本节的内容如下:
仅有授权还不足以保证应用程序的安全,因此,本指南将简要介绍应用程序面临的几种威胁。以下是一些常见的安全威胁,这些威胁通常缩写为“STRIDE”,包括:
您可以使用以下技术来解决 STRIDE 威胁:
有关 STRIDE 的详细信息,请参阅 MSDN Library 中的 Designing for Securability(英文)。
图 2 显示的模型说明了如何降低多层应用程序中的 STRIDE 安全威胁。
图 2:多层应用程序的安全模型
图 2 描述的是一种具有多个物理层的部署,但是,许多较小的应用程序是在一个物理层上完成的,这就简化了身份验证、授权和标识流。图 2 包含了以下降低安全威胁的措施:
您可以使用多种授权机制来控制应用程序的功能,使其按预期方式运行,不被意外或蓄意误用。这些授权机制包括以下几种:
可以综合使用这些方法创建安全的应用程序,如图 3 所示。
图 3:选择授权机制
“系统授权”是为操作系统控制的对象(例如打印机、文件)设置资源权限或 ACL 的过程。系统管理员负责维护这些设置。系统授权是一种非“是”即“否”的决策模式:用户要么被授权访问资源,要么不被授权访问资源。
系统授权的示例包括:
有关系统级安全和授权的详细信息,请参阅 MSDN Library 中的 Building Secure ASP.NET Applications(英文)。
系统授权可以对各种对象施加约束,而限制代码则需要采用 .NET 代码访问安全性授权。
.NET 公共语言运库使用代码访问安全性来限制可执行的代码。代码访问安全性根据证据向应用程序代码授予权限(称为“权限设置”)。这些证据可以包括代码的来源、发布者或其他证据,如程序集的严格名称。
权限设置使您能够控制应用程序可以执行的操作,如删除文件或访问 Internet。例如,您可以限制应用程序只使用隔离的存储单元或控制打印访问。
不管用户的身份如何,代码访问安全性只考虑证据,即使具有管理特权的用户使用应用程序,代码访问安全性权限仍旧保持不变。例如,如果代码来自 Internet,不管用户是谁,对其应用的限制(如删除文件的能力)保持不变。
代码访问安全性的应用示例包括:
注意:请使用 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 提供了基于角色的应用程序授权能力。“角色”指共享同一安全特权的一类或一组用户。
使用角色代替特定的用户标识具有以下优势:
角色可以代表用户在组织中的地位,例如:
使用这种方法的一个好处是信息通常可以从存储库(例如 Active Directory)中检索出来。通常情况下,这些角色在对实际业务需求进行建模时十分有用。
您还可以使用角色来指出用户执行的工作属于哪种操作类型。这样的角色可以将应用程序的功能链接到各个用户,例如:
第二种方法更灵活一些,因为您可以围绕应用程序的功能来设计角色,而不用过多考虑组织的结构,但维护起来可能比较困难,因为缺乏保存角色的结构。大多数情况下,需要在应用程序中综合使用这些方法。
有时您必须以用户是谁作为授权的基础,而不会过分关注用户在应用程序中扮演的角色。例如,您可能要实现只允许部门经理批准员工的费用申请,而要达到这种授权级别,可以将当前用户与提出申请的员工的经理进行比较。
.NET Framework 在 System.Security.Principal 命名空间中提供了基于角色的安全设置实现,您可以用此实现来授权应用程序。要在 .NET Framework 中使用应用程序授权,请创建 IIdentity 和 IPrincipal 对象来表示用户。IIdentity 封装的是一个通过验证的用户,IPrincipal 则是用户标识和用户角色的组合。
图 4 显示了 IIdentity 和 IPrincipal 对象之间的关系。
图 4:IIdentity 和 IPrincipal 对象之间的关系
请注意图 4 中的以下几点:
.NET Framework 提供了四种实现 IIdentity 接口的类:
每种类都允许您使用不同种类的用户标识。要访问使用 Windows 身份验证的应用程序的当前 WindowsIdentity 对象,可以使用 WindowsIdentity 类的静态 GetCurrent 方法,如以下代码所示:
您还可以通过在自己定义的类中实现 IIdentity 接口来创建自定义标识类。有关创建自定义标识的详细信息,请参阅本指南后面的扩展默认实现。有关如何使用默认 IIdentity 实现的详细信息,请参阅本指南后面的设计用于授权的身份验证。
.NET Framework 提供了链接用户角色和标识的 IPrincipal 接口。所有执行应用程序授权的托管代码都应该使用实现 IPrincipal 的类的对象。例如,WindowsPrincipal 和 GenericPrincipal 类提供了内置的 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 对象也应该受到保护,因为应用程序授权能力是以当前的主体为基础的。框架要求这些操作具有代码访问安全性权限,从而提供了保护。使用 Caspol.exe 或 .NET Framework 配置工具为需要管理这些对象的应用程序授予 SecurityPermissionAttribute.ControlPrincipal 权限。
默认情况下,所有本地安装的应用程序均具有该权限,因为它们是在“完全信任”的权限设置下运行的。
执行以下方法需要 ControlPrincipal 权限:
有关使用 CASPOL 设置安全权限的详细信息,请参阅 MSDN Library 中的 Configuring Security Policy Using the Code Access Security Policy Tool (Caspol.exe)(英文)。
公共语言运库在 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 身份验证进行授权的整个过程是这样的:
注意:先将 IPrincipal 对象链接到线程,然后再从 Thread.CurrentPrincipal 属性检索主体或标识信息。这样就可以在内存中维护一个角色集,从而降低查找这类信息的频率。
您可以使用 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:
注意:使用 IIdentity 和 IPrincipal 对象的代码必须具备 ControlPrincipal 权限,请使用代码访问安全性为应用程序授予该权限。有关详细信息,请参阅本指南前面的 在基于 .NET 的应用程序中使用基于角色的安全设置。
AppDomain 对象的 SetPrincipalPolicy 方法可以将特定类型的 IPrincipal 对象链接到应用程序域。请使用 PrincipalPolicy 枚举类型的 WindowsPrincipal 值,将当前的 WindowsPrincipal 对象链接到应用程序线程,如以下代码所示:
然后就可以授权检查主体了。在开始执行应用程序时,调用 SetPrincipalPolicy 以确保在执行任何授权检查之前 IPrincipal 对象已链接到线程。
请勿在 ASP.NET 应用程序中使用 SetPrincipalPolicy,因为在 Global.asax 中定义的 Application_AuthenticateRequest 事件处理程序过程中,ASP.NET 身份验证将为您管理此值。
注意:只有在应用程序域中不存在默认的主体时, SetPrincipalPolicy 才有效。
要设置 CurrentPrincipal 属性,首先必须创建一个新的 WindowsPrincipal 对象,方法是将 WindowsIdentity 对象传递给 WindowsPrincipal 构造函数。然后将新的 WindowsPrincipal 对象链接到 Thread.CurrentPrincipal 属性,如以下代码所示:
然后就可以授权检查 IPrincipal 对象了。
有关如何使用 WindowsPrincipal 执行授权检查的详细信息,请参阅本指南后面的执行授权检查。
要根据应用程序定义的角色(如 CanApproveClaims)创建授权检查,必须手动创建通用的或自定义的 IPrincipal 对象。您无需调用 SetPrincipalPolicy 方法,因为它的默认设置是 NoPrincipal,正是指定您自己的主体的正确设置。
大多数情况下,根据已通过身份验证的用户名来创建 GenericIdentity 对象。(通过使用 IIdentity 接口的 Name 属性,可以得到该用户名。)这样,在向其他组件提供 IIdentity 对象时,就可以避免所有涉及用户登录令牌(可以使用 WindowsIdentity 对象访问)的安全问题。然后,可以创建 GenericPrincipal,将 IIdentity 对象链接到自己的应用程序定义的角色列表中。
以下代码显示了使用 Windows 身份验证时,如何创建 GenericIdentity 和 GenericPrincipal 对象:
大多数情况下,应该从数据存储库中检索应用程序定义的角色,而不是为角色编写代码,从而在指定角色成员时获得更大的灵活性。有关使用该方法的示例,请参阅附录中的如何使用 SQL Server 构建 GenericPrincipal。
有关如何使用 GenericPrincipal 执行授权检查的详细信息,请参阅本指南后面的执行授权检查。
您可能不想或不能使用 Windows 身份验证执行应用程序授权。出现这种情况的原因可能是用户没有 Windows 用户帐户,或者是因为技术限制而无法使用 Windows 身份验证来验证用户的凭据。
非 Windows 身份验证是使用非 Windows 技术验证用户凭据的过程。这种身份验证包括 .NET Framework 为您提供的验证(如 ASP.NET 窗体身份验证)和您自己创建的验证方法。
使用非 Windows 身份验证进行授权的整个过程如下:
常见的非 Windows 身份验证类型有:
窗体身份验证提供程序根据 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 身份验证提供程序根据 PassportIdentity 对象创建一个 GenericPrincipal,但是没有角色成员关系。
有关 Passport 身份验证的详细信息,请参阅 MSDN Library 中的 The Passport Authentication Provider(英文)。
HTTP 传输提供用于构建通用或自定义的 IIdentity 对象和 IPrincipal 对象的信息(如 cookie)。
有关 ASP.NET 的身份验证技术的详细信息,请参阅 MSDN Library 中的 Authentication in ASP.NET: .NET Security Guidance(英文)。
以上列举的所有非 Windows 身份验证类型均涉及到 GenericPrincipal 对象或自定义 IPrincipal 对象。关于如何将 GenericPrincipal 链接到应用程序定义的角色的示例,请参阅附录中的如何使用 SQL Server 构建 GenericPrincipal。
身份验证创建用于授权的 IIdentity 和 IPrincipal 对象,并确定如何通过编程方式将标识信息传递给其他计算机上远程部署的应用程序逻辑。已通过验证的标识的传递过程称为“标识流”。实现标识流的方法有两种:
如果所有需要标识信息的代码是在同一上下文中执行的,公共语言运库将自动提供标识流。调用者和被调用者共享相同的应用程序域时,它们处于同一上下文。如果客户端代码(称为“调用者”)和被调用的组件(称为“被调用者”)在同一上下文中运行,.NET 将对两者自动使用同一个 Thread.CurrentPrincipal 对象。有关被调用者和调用者处在不同线程的情况,请参阅本章后面的使用多线程执行授权。
在一个应用程序域中执行的代码示例包括:
由于技术限制,需要在不能执行身份验证的远程层进行授权检查时,可以实现手动标识流。例如,通过 TCP 通道使用 .NET Remoting 调用远程组件。有关在 .NET Remoting 应用程序中使用通道的详细信息,请参阅 MSDN Library 中的 Microsoft .NET Remoting: A Technical Overview(英文)。
如果您正在实现自己的标识流,请使用与调用代码不同的方式传递数据(称为“分离”)。如果您的组件接口涉及调用功能,并且使用参数传递数据,请使用其他方式发送标识信息。
例如,如果您正在使用 SOAP 调用 XML Web Service,则请勿将标识信息作为消息正文的一部分发送,而应将其放入自定义的 SOAP 标头中。这时对安全性的要求不象处理代码设计问题时那样严格。这种分隔方式可以增加代码接口的扩展性、可复用性和聚合性。这样做还使您可以随着技术标准的发展更改发送标识信息的方法,而无需改变接口约定。Web Services Security (WS-Security)(英文)规范说明了这一点。
请注意,标识流不同于传递凭据,后者是在服务器上进行的高效重验证过程。另外,您还可以在消息本身中传递已加密的凭据,但是,加密需要使用共享密钥或公钥系统,所以用这种方式传递凭证没有什么优势。
以下是一些实现标识流的最佳方案:
在信任环境中,被调用者将身份验证的责任交给了调用者。这时认为我们从调用者传递到被调用者的所有信息都是安全可靠的,例如 Web 层是调用者,内部应用程序中的组件是被调用者。
注意:如果非法用户可以调用其他(和潜在虚假)标识传递的代码,这种解决方案将是不安全的。为了防止这类欺诈攻击,请使用系统层提供的技术来保护安全套接字层 (SSL) 或 IP 安全性 (IPSec) 通信通道,并使用代码访问安全性来确保只有相应的代码才能调用您的函数。
在信任环境中,主要采用两种方法在应用程序层之间传递标识信息:
当您向数据库传递标识信息进行审核时,也可以使用这种方法。有关详细信息,请参阅本指南后面的在数据层执行授权。
该方法较为灵活,因为不需要强制调用者传递特定类型的对象或额外信息,例如应用程序角色。
通过传递自定义的 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 解决方案。
上面您已经了解了如何使用各种身份验证机制实现授权,以及如何在各层之间传递授权数据,下面将为您介绍如何在企业应用程序的各层进行授权。
目前大多数的应用程序都得益于多层设计,因为多个层次可以提供伸缩性、灵活性并且可以改善性能。尽管应用程序各层的授权目的相同(控制用户访问系统功能和数据),但是各层授权的设计方法和实现方法并不相同。企业应用程序中的三层是:
本节介绍在用户界面层执行授权的方法。本指南的后面将介绍以下主题:在业务层执行授权和在数据层执行授权。
在用户界面层执行应用程序授权,有助于确保只有得到授权的用户才可以查看和修改数据,或者执行与特定岗位相关的业务功能(例如与工资有关的操作)。对于还需要在系统的其他层进行授权的过程来说,在用户界面层授权用户,是限制对这些过程的访问的第一道关卡。
要实现以下目的时,请在用户界面层创建访问检查:
在用户界面层创建授权代码时,请使用以下最佳方案:
注意:以下主题的所有代码示例均以 Web 窗体为例。可以通过将 User.IsInRole 更改为 Thread.CurrentPrincipal.IsInRole,使代码适用于 Windows 窗体。有关 IsInRole 方法和执行授权检查的详细信息,请参阅本指南后面的 执行授权检查。
在 Windows 窗体和 ASP.NET 应用程序中,都有可能需要根据授权改变控件在用户界面中的显示方式,包括改变某些控件的可见性或者禁用它们。
例如,在雇员费用申请的应用程序中,CanApproveClaims 角色的成员需要在窗体中使用一个按钮批准申请,而所有不属于该角色的用户都看不到这个 Approve 按钮。以下代码说明实现该目的的方法:
如果用户界面比较复杂或者有很多自定义的角色,那么在创建授权代码时,以上这种过于简单的方法会导致“if-then-else”语句链过长。
您可以使用以下技术来降低这种根据授权检查显示控件的复杂性:
以下代码显示了如何对 Web 窗体应用这种方法:
例如,如果您创建了一个使用 DataGrid 的 Web 窗体,则可以使用一个复选框模板列,允许已授权的用户同时批准多项申请。
以下代码说明了如何使用该技术在 DataGrid 中显示 Approve 列。
将加载窗体之后改变控件状态的授权代码放在这些控制器函数中,而不要放在事件处理程序中。
有时(比如典型向导的用户界面)会根据用户的角色成员关系,要求用户完成多个窗体(或页面)以执行业务流程。这种方法可以改变页面的顺序或“流”。
在上面的申请示例中,GeneralManager 成员可以查看并批准所有部门的员工申请,而 StoreManager 成员只能查看自己部门的员工申请。为了使 GeneralManager 成员可以选择部门,需要另外创建一个步骤来检索该信息,然后再显示员工选择窗体。
以下代码说明了创建这种页面流的方法。这段代码使用 ASP.NET Server.Transfer 方法向不同的页面传递控制。对 Server.Transfer 的调用包含在名为 DisplayNextPage 的私有方法中:
业务过程可以是很简单的业务逻辑,也可以是大型的、运行时间很长的、包含许多信任和非信任伙伴的工作流程。业务层代码必须保证所有的客户请求都能满足组织规定的授权规则和逻辑规则。
您有如下需求时,请在业务层中创建访问检查:
在业务层创建授权代码时,请使用以下最佳方案:
但要使解决方案更加安全,需要在每个可以公开访问的方法中执行授权,因为对这些方法的调用可能未通过授权检查。
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),以及应用程序中自定义的实体类,具体使用哪一类取决于应用程序的物理和逻辑设计限制。应用程序既生成实体又使用实体。
实体经常在应用程序层之间移动,包括从数据源检索数据、发送到用户界面再返回到数据源进行更新的整个过程。
您可以创建类来代表执行授权检查的实体。例如,只有属于某一角色的用户才能创建和初始化实体对象。
但是,大多数情况下,不应在实体中执行应用程序授权,因为:
服务接口是将内部业务逻辑呈现给外部世界的窗口。服务接口向业务流程提供了一个入口点,使您可以灵活地改变内部流程的方法签名,而不影响外部调用者。
非信任方或要求身份验证和授权的一方访问内部业务流程组件之前,服务接口为其提供入口。
在费用申请的示例中,服务接口使用 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 对象,从而将标识流传递到组件之外。有关管理标识流的详细信息,请参阅本指南前面的设计用于授权的标识流。
数据层是最后一道防线,用于抵御来自用户界面、业务组件以及试图直接访问数据的攻击者的非法请求。从授权的角度看,数据层确保只有经过批准的用户才可以访问和修改数据,而不管其访问方式如何。
需要执行以下操作时,请在数据层中创建访问检查:
要在数据层执行授权,可以使用多种技术和对象,这取决于您的安全要求。可以在以下数据层组件中执行授权:
有关使用数据访问逻辑组件和实体的详细信息,请参阅 MSDN Library 中的 Designing Data Tier Components and Passing Data Through Tiers(英文)。
以下这些问题能帮助您选择数据层的授权策略:
如果创建执行相似操作的多个存储过程会增加维护的难度,则可以向存储过程传递一个值以减少存储过程的数量。
在数据层创建授权代码时,请使用以下最佳方案:
有关连接管理的详细信息,请参阅 MSDN Library 的 .NET Data Access Architecture Guide(英文)中的“Managing Database Connections”。
拒绝对数据表的直接访问可以使用应用程序(如查询分析器)防止连接到数据库的用户,而不会强制对数据的业务逻辑进行更新或查询。只有允许的用户/服务帐户可以访问存储过程和视图。
数据访问逻辑组件是在访问数据库之前最后一个提供功能的组件。因此,您可以利用它们来确保只有获得授权的用户才可以访问或修改数据。
出现以下情形时,请在数据访问逻辑组件中执行授权:
在数据访问逻辑组件中使用基于角色的安全设置的例子包括:在数据传递到其他应用程序层之前对数据进行过滤、控制代码执行的存储过程。
有关如何执行授权检查的详细信息,请参阅本指南后面的执行授权检查。
为了保证只向其他应用程序层返回适当的信息,应该在数据访问逻辑组件代码中有选择地删除敏感数据。
下面的代码示例在用户不属于 Manager 角色时,删除数据的 ConfidentialNotes 列。
从数据库中检索不需要的列是对资源的一种浪费,尤其是不需要的列很多时。相反,使用存储过程只选择必要的数据,可以改善数据库引擎的性能。
您可以创建仅为一个角色返回正确数据的多个存储过程。然后,数据访问逻辑组件代码执行授权检查,根据用户角色确定要使用哪些存储过程。
例如,可以为 Manager 创建一个存储过程版本,为 Employee 创建另一个不同的版本,两者只是检索的数据不同。数据访问逻辑组件代码将检查用户的角色成员关系,并调用正确的存储过程。
以下代码显示了数据访问逻辑组件方法如何根据用户的角色成员关系选择要调用的存储过程。这些代码使用了一个辅助类,以避免显示过多的 ADO.NET 代码。Microsoft Application Blocks for .NET(英文)提供了 SqlHelper 类:
在存储过程中执行授权的方法主要有两种:
以数据库安全功能作为安全解决方案的基础。然后在数据库安全功能之上使用存储过程设计应用程序授权,如本指南前面所述。
使用数据库安全功能执行授权:
在数据库中执行授权之前,必须选择应用程序连接到数据库的方式。
设计系统时,必须考虑使用 SQL Server 角色简化对 SQL Server 的维护。SQL Server 提供了多种角色,包括:
有关 SQL Server 安全性、授权和授权角色的详细信息,请参阅 MSDN Library 中的 Building Secure ASP.NET Applications(英文)。
考虑将应用程序角色链接到 SQL Server 角色,使应用程序和 SQL Server 的管理一致。将直接映射到 SQL 角色的服务帐户连接到数据库,以实现这种链接。下面一节介绍了这种技术。
为了控制对表、列、视图、存储过程和其他数据对象的访问,SQL Server 必许能够识别需要授权的用户,这种识别可以通过以下连接方法实现:
这种方法提供的连接最安全,因为它不通过线路传递凭据。
无论以何种方式连接到数据库,都要先创建几个代表应用程序角色的服务帐户,然后根据角色要求,为各个服务帐户授予权限。
例如,创建 USR_MANAGER 和 USR_APPROVER 等服务帐户以实现最少特权的原则,即只允许访问一部分应用程序功能。
这种方法会带来某些不便,因为当您使用 Windows 身份验证连接到数据库时,需要从数据访问逻辑组件模拟服务帐户。
有关管理数据库连接的详细信息,请参阅 MSDN Library 中的 .NET Data Access Architecture Guide(英文)。
使用 T-SQL 语句或 SQL Server 企业管理器实用程序,在列级别应用 GRANT、DENY 和 REVOKE 权限,从而实现对列的访问控制。请尽量少用这种授权技术,因为如果数据库很大,要维护的列权限列表会很长,在实际操作中非常耗时。如果您要使用这种技术,请在连接到数据库时使用服务帐户,以使维护工作易于管理。
SQL Server 没有隐含地让您控制对各行的访问。但是,您可以向数据表添加安全列,创建控制行的功能。安全列可以保存单独的用户名或访问数据需要的最低角色(假设角色的组织形式是分层的)。安全列允许您限制对各行的访问,但是如果用户可以直接访问数据表,该技术将没有效果。
可以结合使用视图或存储过程中数据表的安全列功能和 SUSER_SNAME 函数,控制对各行的访问。SUSER_SNAME 函数将检索连接的用户名(很可能是服务帐户名)。
以下代码显示如何创建一个视图,该视图只返回当前用户的行。
注意:该方法要求您在连接数据库时使用单独的用户标识,以便 SUSER_SNAME 函数能够检索到正确的信息。在这种情况下,应用程序将无法利用连接池的优势。
本节介绍如何使用基于角色的 .NET 安全设置来编写执行授权的代码。基于角色的安全设置可使您对整个用户类别或用户集执行授权,而不只是对特定用户执行授权。
您必须考虑以下问题:
.NET Framework 提供了三种方法来检查某个主体是否是角色的成员:
本主题将为您逐一介绍这些技术,并对其各自的使用场合提供指导。上述三种技术提供的性能相同,因为它们都使用执行线程的主体的 IsInRole 方法。同样,这三种技术还都假设主体已自动或手动链接到 Thread.CurrentPrincipal 属性,如本指南前面的设计用于授权的身份验证中所述。
但是,在检查某个主体是否为角色的成员这个问题上,这些技术之间存在一些明显的差别。例如:
在这三种方式中,手动检查角色是最常见的方法。
可以通过调用 IPrincipal 对象的 IsInRole 方法,手动检查某个用户是否属于某个角色。但是,GenericPrincipals 和 WindowsPrincipals 的 IsInRole 是不一样的。
当手动使用 GenericPrincipals 检查角色时,必须使用一个字符串来表示角色名。例如,以下代码将检查用户是否属于 Claims Approver 角色。
当使用 WindowsPrincipals 手动检查角色时,有两种不同的方法表示角色名:
如果是本地计算机级别的组,请使用 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 格式“[email protected]”。
这种枚举可以简化本地化代码的过程,因为 Windows 安装环境不会影响用户组列表。
不管您使用的是 GenericPrincipal 还是 WindowsPrincipal,都可以在 C# 语言中通过使用包含逻辑运算符(如逻辑 AND [&&] 和逻辑 OR [||])的多段 IF 语句执行复杂的访问检查。以下代码显示了这样的检查:
您可以通过组合运算符,如小于号 (<) 和大于号 (>),来执行复杂的检查。
通过抛出异常来处理授权故障。有关传递授权检查故障的详细信息,请参阅本指南后面的抛出授权异常。
您可以使用命令式检查来强制某一用户属于某种特定的角色。通过创建 PrincipalPermission 对象并指定所需的角色,对用户的角色成员关系进行命令式检查。然后由 Demand 方法执行访问检查。如果主体不是命令角色的成员,则公共语言运库将生成一个 SecurityException。
以下代码检查活动的主体是否属于应用程序指定的角色 CEO。
注意:本主题中的所有示例都向 User 属性的 PrincipalPermission 构造函数传递了一个 null 值,以使属于相应角色的用户通过访问检查。如果您指定了用户名,运行时将检查特定的用户,这将影响使用角色的目的。
您可以使用以下技术对角色成员关系进行命令式检查:
注意: PrincipalPermission 类也有一个 Intersect 方法,但是该方法不用于角色检查,因为两个用户永远不会相交。该方法仍存在的原因是实现 IPermission 接口时要用到它。
您可以使用标准的异常处理技术来处理调用 Demand 失败时生成的异常。有关传播授权检查失败的详细信息,请参阅本节的处理授权错误。
您可以将 PrincipalPermissionAttribute 放到类、方法和属性上,公开要求角色权限,如以下代码所示。请注意,成员级属性会覆盖所有类级属性。
在编译过程中,.NET 编译器将 PrincipalPermissionAttribute 保存为程序集元数据中的 XML。在应用程序执行过程中,运行时实例化 PrincipalPermission 对象并执行访问检查。因此,命令式和声明式的访问检查的功能类似。
在通过授权检查之前,运行时不会为调用创建代理,也不会封送任何参数,因此在授权检查失败时性能会有所提高。如果检查失败,运行时将自动抛出 System.Security.SecurityException,受保护的方法代码均不能执行。由于方法均不能执行,所以不能根据业务逻辑执行更复杂的授权,例如比较参数值。
在编译时,运行时将计算属性的值,因此您必须在元数据中对角色硬编码,这样就不能使用诸如 Environment.MachineName 之类的值实现动态解决方案。您也不能使用 WindowsBuiltInRole 枚举,因为声明式检查使用的是 IPrincipal 基本实现。
如果指定多个 PrincipalPermissionAttribute,则这些属性将组合起来形成 OR 方案。因此,只要主体是某一角色的成员,授权检查就能成功。以下代码要求该方法的调用者属于 CEO 或 Senior Manager 角色:
注意:不能使用声明式方法检查用户是否属于某个角色列表。
使用声明技术有助于将授权代码与其他业务逻辑分开。将业务逻辑和授权要求分开可以改善代码的可维护性。做出这种分离之后,改变授权逻辑时不用改变业务逻辑,从而降低了由于疏忽而在代码中引入业务逻辑错误的可能性。
基于角色的 .NET 安全框架的设计人员对 IIdentity 和 IPrincipal 接口进行了简单的定义,目的是使开发者可以对接口进行扩展以满足自己的需要。出现以下情形时,可能需要扩展该功能:
例如,有可能需要保存组织地址代码等信息,以说明用户是否位于特定的地点。
.NET Framework 提供了一种从 Windows 检索角色信息的实现(使用 WindowsIdentity 和 WindowsPrincipal 类)。如果您要维护自己的信息存储库,通常需要实现自定义的扩展。
如果业务过程中的授权检查失败,组件代码需要通知失败的调用者。最好的通知方法是抛出一个异常。如果您正在实现一个异步解决方案,那么处理这种异常只要采取处理标准异步异常的方法即可,例如在使用 MSMQ 时向异常队列中插入一个异常。
在您设计组件时,假定在信任边界的“看门人”处已进行了相应的授权检查。如果违反了这种假设,组件应抛出异常。例如,您可以假设用户界面不允许非 Administrator 角色的用户删除文件。应用程序创建用户界面时,会在入口处实施有效的检查,而执行删除文件逻辑的组件必须遵循这种假设。
当应用程序授权检查失败时,可以抛出以下类型的异常:
当命令式或声明式授权检查失败时,运行时会自动抛出这种异常。
如果您认为授权失败会导致安全违例,可以在应用程序中使用这种异常。这种异常类型可用于捕捉某个 catch 处理程序中的所有与安全相关的异常。
代码访问安全性和其他系统级的操作也会抛出该异常,这会增加调用者过滤异常和采取相应措施的难度。
您可以按照以下方法,创建有益于应用程序的自定义异常类:
有关创建自己的异常及其实现示例的详细信息,请参阅附录中的如何创建授权自定义异常类型。
注意:传播的授权异常不应包含任何可能损坏系统的敏感信息。有关这方面的信息和其他异常管理最佳方案的详细信息,请参阅 MSDN Library 中的 Exception Management in .NET(英文)。
除了基于角色的授权异常以外,调用代码还可以为授权异常定义 catch 处理程序。采用的部署方案不同,您收到的 SecurityException 违例的形式可能也不同。例如,如果您从半信任环境启动代码,运行时可能抛出异常,表示代码访问安全性权限失败(如文件访问问题)。尽管这些异常与基于角色的授权不相关,但是可能导致 SecurityException。
可以编写异常处理代码来区分基于角色的授权异常和其他类型的授权异常。使用 SecurityException 对象的 PermissionType 属性来检查异常是否与基于角色的授权相关,如以下代码所示:
PermissionType 属性的结果允许您过滤代码访问安全性和基于角色的授权异常。过滤异常之后,您可以抛出自定义的异常或在被调用者内部处理异常。
大多数基于 .NET 的应用程序都使用多线程以改进响应性或异步地执行工作。使用主体时,多线程会使问题变得复杂,因为每个线程都必须链接到一个 IPrincipal 对象。运行时将 IPrincipal 对象复制到父线程创建的所有新线程中。
根据以下规则,Thread.CurrentPrincipal 属性返回一个不同的 IPrincipal 对象:
调用 AppDomain.CurrentDomain.SetThreadPrincipal 方法,可以保证运行时为当前的和新的线程使用特定的 IPrincipal 对象。对于 ASP.NET,建议您不要使用此选项,因为主体由运行时处理,而且此选项还会影响由其他用户运行的其他线程。
注意:如果在应用程序域的有效期内多次调用此方法,运行时将抛出一个 PolicyException。
当扩展基于 .NET 的应用程序授权框架时,请遵循以下原则:
决定要创建的扩展类型以及在什么位置创建。表 1 包含一些常见的扩展,并且说明应该在哪里创建需要的附加逻辑。
表 1:创建应用程序授权扩展的位置扩展类型 | 自定义标识 | 自定义主体 | Helper 类 |
---|---|---|---|
用户指定的信息 | X | ||
相关的权限或角色 | X | ||
需要的外部知识 | X |
当 GenericIdentity 或 WindowsIdentity 对象提供的功能不能满足您的需要时,您应该创建自己的类来实现 IIdentity。由此可能产生的增强功能包括:
例如,使用 ISAPI 单一登录解决方案时,您可以创建自己的自定义标识。
当 GenericPrincipal 或 WindowsPrincipal 对象提供的功能不能满足您的需要时,您应该创建自己的类来实现 IPrincipal。由此可能产生的增强功能包括:
如果在设计框架时考虑到重复使用,您就可以在多个应用程序中重复使用授权框架。可以重复使用的内容一般包括用于实现授权能力的代码和结构。
重复使用应用程序授权框架的好处有:
本节介绍重复使用授权实现的一些最佳方案。
您可以选择要在应用程序中重复使用的应用程序授权框架数量。选项有:
图 6 显示了实现应用程序授权重复使用的示例。
图 6:应用程序授权重复使用的逻辑表示
请注意图 6 中的以下几点:
表 2 描述了图 6 中显示的各个组件,并解释了在不同项目和不同应用程序中重复使用这些组件的方法。
表 2:如何重复使用授权组件组件 | 说明 | 重复使用的方法 |
---|---|---|
授权框架 | 创建的自定义代码,用于支持授权能力,如自定义主体、标识和异常。 | 创建包含所有自定义的、可重复使用的代码的程序集,以便用于其他工程。 还可以为程序集赋予严格名称,并将程序集安装到全局程序集缓存中。 |
授权缓存 | 一种可选的保存授权信息的单元,距离使用它的框架所处的实际位置很近。 | 使用适当的方法创建 Hashtable 对象(或内存存储库中的类似对象),按照需要进行刷新。为了提高效率,IPrincipal 对象应使用授权缓存检索角色成员关系,而不是进入数据库。 如果需要保护或隐藏缓存中的数据,则使用加密;否则,就对缓存中的数据进行签名以防止数据被篡改(如果数据保存在本地或其他易受攻击的位置,这一点就尤其重要)。 |
授权服务 | 到授权数据的接口(或“看门人”) | 创建一个进程外组件,在可以访问授权存储的安全环境中运行。进程外组件可用于授权对存储库的访问,方法是检查访问是否是由指定帐户进行的,从而防止其他用户访问存储库。 另外,您也可以构建一个集中复用的 XML Web Service,这样提供的互操作性最高。 请记住保护授权服务和授权框架之间的通信通道。 |
管理实用程序 | 应用程序管理员用来维护授权数据的用户界面。 | 创建一个管理员可以调整授权设置的中央网站, 也可以创建一个可重复使用的图形用户界面,部署在各个应用程序中。 |
授权数据 | 授权数据方案、存储过程和数据访问逻辑组件的定义 | 对所有授权数据使用一个数据库。 另外,所有的应用程序都使用标准的授权方案。 |
为了保证可重复使用的授权框架的安全性,请遵循以下原则:
使用以下技术改进可重复使用的应用程序授权框架的性能:
本附录包括以下主题:
.NET Remoting 组件不会自动将主体从一个应用程序域传递到另一个应用程序域,除非这两个域在同一过程中。您必须手动在调用者和被调用者之间传递此信息,并保证在传递过程中信息处于安全状态。
有关使用 Windows 身份验证的 .NET Remoting 的详细信息,请参阅 MSDN Library 中的 .NET Remoting Security Solution(英文)。
在使用非 Windows 身份验证时,可以使用 CallContext 类和 ILogicalThreadAffinative 接口提供标识流。
当远程对象调用方法时,CallContext 类允许包含附加信息。该信息可以是单个方法要求的任意信息,例如用户信息。
只有实现 ILogicalThreadAffinative 接口的类型才被发送到单独应用程序域中的服务器。例如,您可以创建一个实现 ILogicalThreadAffinative 的简单类型,存储基于 IIdentity 或 IPrincipal 的对象。远程对象可以访问此对象,并且您可以检查角色的成员关系。
以下代码显示了如何创建实现 ILogicalThreadAffinative 接口和包含基于 IPrincipal 的对象的类。
以下代码显示了远程对象如何使用 CallContext.GetData 方法,通过 PrincipalStorage 类来检索 IPrincipal 信息。
以下代码显示了客户端应用程序如何使用 CallContext.SetData 方法在调用远程对象之前设置 IPrincipal 信息。
以下代码显示了注册远程对象的服务器应用程序。
在下面的代码中,ProcessClaim XML Web Service 方法执行授权检查,并在检查失败时抛出异常。检查将确定费用申请的发出者是否被授权发送申请。
为了确保正在处理的被调用者已通过身份验证,需要指定 PrincipalPermissionAttribute 的 Authenticated 属性。这样做还可以确保 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 应用程序中的默认 IPrincipal 对象,使其包括在身份验证发生之后从数据存储库加载的特定于应用程序的角色。更改 IPrincipal 对象之后,您可以继续使用应用程序定义的角色进行授权检查,同时仍然可以依赖任意类型的授权。最好在 Application_AuthenticateRequest 事件处理程序中更改 IPrincipal 对象,因为可以使用通过验证的标识,而且事件处理程序是在网页或 XML Web Service 逻辑之前执行的。
以下代码根据通过验证的标识用 GenericPrincipal 对象代替 HttpContext.Current 的 IPrincipal 对象,并向 HttpContext.Cache 添加对象以改进性能:
该示例使用缓存来减少查询数据库的次数。示例中之所以选择缓存是因为缓存允许您设置超时值。但是,如果使用会话状态,则可以减少测试过程,因为 ASP.NET 可以保证用户只访问自己的会话。使用此类代码时必须小心,确保不要用错误的主体引发运行用户请求的线程。有关 GetPrincipal 方法的实现示例,请参阅下一主题。
以下示例显示了一个简单的数据库方案,可以用来导入带有角色的 GenericPrincipal。运行以下代码创建 UserRoles 表:
其后的 GetPrincipal 方法带有 IIdentity 参数,向 GenericPrincipal 提供标识信息。该代码使用标识名在 UserRoles 表中查找角色,以便将角色导入 IPrincipal 对象。
请注意,该代码将标识名作为字符串传递到 GenericIdentity 构造函数,而没有使用 IIdentity 参数进行传递。将标识名作为字符串传递,使得代码可以与任意类型的身份验证结合使用。
代码还向 GenericIdentity 构造函数传递了另一个字符串,表示最初使用的身份验证类型。
有关调用 GetPrincipal 方法的代码示例,请参阅附录中前面的如何在 ASP.NET 应用程序中更改 Principal。
.NET 应用程序使用 COM+ 来提供基于角色的安全设置时,可以使用以下两种方法之一检查角色成员关系:手动检查或声明式检查。
执行手动检查或声明式访问检查
使用 AccessChecksLevelOption.ApplicationComponent 值执行应用程序级和组件级检查。
注意:步骤 1、3 和 4 实际上并不需要在代码中实现,它们可以在组件服务管理控制台中完成。
完成组件服务应用程序的配置
注意:要使应用程序能够正常工作,您可能还需要增加几个步骤,如在全局程序集缓存中安装程序集。这些步骤与应用程序部署相关,本指南不作介绍。有关安装组件服务应用程序的详细信息,请参阅 MSDN Magazine 中的 COM+ Integration: How .NET Enterprise Services Can Help You Build Distributed Applications(英文)。
手动角色检查依赖于安全性是否可以调用应用程序代码中的上下文对象。如果启用组件服务应用程序的安全性,则上下文对象可用。
手动检查 COM+ 角色成员关系时,请使用以下技术:
以下代码显示了如何测试安全性并检查角色成员关系。
声明式检查启用属于特定角色的用户,以激活组件并调用其任一公共方法。这种类型的角色检查与在类级指定基于角色的声明式 .NET 安全属性相类似。在类级使用 SecurityRoleAttribute 指定可以访问类的角色,如以下示例所示。
对方法执行声明式检查需要的步骤比类级检查多:
以下代码演示了这些步骤。
衷心感谢以下各位参与撰稿和校对的专家: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。