作为一名 Microsoft 服务顾问,我定期与客户和合作伙伴一起进行应用程序安全性讨论。 在本文中,我将介绍一些在这些讨论中提出的主题。 特别是,我将重点介绍编程人员在尝试保护 Silverlight 应用程序的安全时所面临的新挑战,而且我将考虑开发团队应该将其资源集中于哪些方面。
本文提到了许多技术概念,您可以在其他位置(包括本杂志)找到这些概念的更多详细信息。 因此,我就不在技术层面更加深入地讨论这些主题。 本文的目标是“理清头绪”并介绍如何利用这些概念保护您的应用程序的安全。
当规划应用程序的安全性时,考虑三个 A 非常有用:身份验证 (Authentication)、授权 (Authorization) 和审核 (Audit)。
身份验证是确认用户身份的行为。 我们通常使用用户名和密码执行此操作。
授权是指在进行身份验证之后,确认用户实际上具有执行特定操作或访问特定资源的适当权限的过程。
审核是维护活动记录,以便用户无法拒绝对系统执行的操作和请求的行为。
在 Silverlight 应用程序上下文中,我将重点介绍前两项(身份验证和授权)。 由于这是一个富 Internet 应用程序 (RIA),因此本文中描述的大多数概念同样适用于异步 JavaScript 和 XML (AJAX) 或其他 RIA 方法。 我还将讨论如何防止对您的 Silverlight 应用程序文件进行不必要的访问。
拓扑
Silverlight 是一种跨浏览器插件,其利用 Windows Presentation Foundation (WPF) 率先采用的许多图形概念,使 Web 开发人员能够创建丰富的用户体验,这些用户体验将超出仅使用 HTML 和 JavaScript 创建的体验。
与 ASP.NET 不同的是,Silverlight 是一种客户端技术,它在用户的计算机上运行。 因此,Silverlight 开发无疑与 Windows 窗体或 WPF 有许多共同之处,而与 ASP.NET 的共同之处相对较少。 在许多方面,这是 Silverlight 的最大优势之一,因为它消除了 Web 应用程序的无状态性所导致的许多问题。 不过,由于所有 UI 代码都是在客户端计算机上运行的,因此您不能再相信它。
服务
与 Windows 窗体不同的是,Silverlight 在浏览器沙盒内运行且拥有的功能减少,因此它所提供的安全程度提高(尽管在 Silverlight 4 中,用户可以将某些应用程序标识为可信并将程序的权限提升为允许 COM 互操作)。 正因为如此,Silverlight 不能直接连接到数据库,您必须创建一个可提供对您的数据和业务逻辑的访问的服务层。
例如,您通常会将这些服务承载于您的 Web 服务器上,就像使用 ASP.NET Web 窗体一样。 假定 Silverlight 代码运行于服务器与现实世界之间的信任边界的可信度较差的一侧(参见图 1),您的团队的工作重点应始终是保护服务的安全。
图 1 Silverlight 运行于信任边界的可信度较差的一侧
在您的 Silverlight 代码内实现严格的安全检查几乎没有意义。 毕竟,攻击者可以很容易就完全摆脱 Silverlight 应用程序并直接调用您的服务,从而避开您实现的任何安全措施。 此外,恶意人员可以使用像 Silverlight Spy或 Debugging Tools for Windows 这样的实用程序更改您的应用程序在运行时的行为。
我们要认识到的重要一点是:服务无法确切地知道哪个应用程序正在调用它或者该应用程序在某些方面尚未被修改。 因此,您的服务必须确保:
- 调用方已经过适当的身份验证
- 调用方已获授权执行所请求的操作
鉴于上述原因,本文的大部分内容重点介绍如何采用与 Silverlight 兼容的方式保护服务的安全。 特别是,我将考虑通过 ASP.NET 在 Microsoft IIS 中承载两种不同类型的服务。 第一种类型是使用 Windows Communication Foundation (WCF)创建的服务,它为构建服务提供一种统一的编程模型。 第二种类型是 WCF 数据服务(以前称为“ADO.NET 数据服务”),其构建于 WCF 之上,允许您使用标准 HTTP 谓词(一种称为“具象状态传输”(REST)的方法)快速公开数据。
通常,如果担心安全性,则加密客户端和服务器之间的任何通信始终是明智之举。 建议使用 HTTPS/SSL 加密,且本文内假定使用此加密方法。
目前,Web 开发人员在 Microsoft 平台上最常用的两种身份验证方法是 Windows 身份验证和窗体身份验证。
Windows 身份验证
Windows 身份验证利用本地安全机构或 Active Directory验证用户凭据。 这在许多方案中都是一大优势;它意味着您可以使用系统管理员已经熟悉的工具集中管理用户。 Windows 身份验证可以使用 IIS 支持的任何方案,包括基本身份验证、摘要式身份验证、集成身份验证(NTLM/Kerberos)和证书。
在使用 Windows 身份验证时,通常都会选择集成方案,因为用户无需再次提供其用户名和密码。 用户在登录到 Windows 之后,浏览器可采用用于确认个人身份的令牌或握手形式转发凭据。 但是由于客户端和服务器需要了解用户的域,使用集成身份验证有许多缺点。 因此,集成身份验证最适用于 Intranet 方案。 此外,尽管它自动与 Microsoft Internet Explorer 一起使用,但其他浏览器(如 Mozilla Firefox)需要进行额外配置。
通常,基本身份验证和摘要式身份验证需要用户在启动与您的网站的会话时,重新输入其用户名和密码。 但是,由于这两种身份验证都属于 HTTP 规范,因此它们在大多数浏览器中均可正常使用,即使是从组织外部进行访问也是如此。
Silverlight 利用浏览器进行通信,因此使用刚才讨论的任何 IIS 身份验证方法,均可轻松实现 Windows 身份验证。 有关如何实现的详细说明,建议阅读分步指南“如何:在 Windows 窗体中,使用 WCF 中的 basicHttpBinding 进行 Windows 身份验证并使用 TransportCredentialOnly”(网址为:msdn.microsoft.com/library/cc949012)。 此示例实际上使用 Windows 窗体测试客户端,但相同的方法也适用于 Silverlight。
窗体身份验证
窗体身份验证是一种为 ASP.NET 中的自定义身份验证提供简单支持的机制。 因此,它特定于 HTTP,这意味着它也可在 Silverlight 中轻松使用。
用户输入用户名和密码组合,此信息将提交给服务器进行验证。 服务器根据可信的数据源(通常是用户数据库)检查凭据,如果凭据正确,则返回一个 FormsAuthentication Cookie。 然后,客户端在随后的请求中提供此 Cookie。 Cookie 经过签名和加密,因此只有服务器才能解密,恶意用户既无法解密,也无法篡改。
调用窗体身份验证的确切方式因登录屏幕的实现方式而异。 例如,如果在验证了用户的凭据后,使用重定向到您的 Silverlight 应用程序的 ASP.NET Web 窗体,您可能不再需要执行身份验证工作。 Cookie 已发送到浏览器,且每当请求该域时,您的 Silverlight 应用程序都将继续使用该 Cookie。
不过,如果您希望在 Silverlight 应用程序内实现登录屏幕,您将需要创建一个公开您的身份验证方法并发送相应 Cookie 的服务。 但幸运的是,ASP.NET 已经提供了您所需要的身份验证服务, 您只需在您的应用程序中启用它即可。 有关详细指南,建议阅读“如何:使用 ASP.NET 身份验证服务通过 Silverlight 应用程序登录”(网址为:msdn.microsoft.com/library/dd560704(VS.96))。
ASP.NET 身份验证的另一项强大的功能是其可扩展性。 成员资格提供程序描述了用于验证用户名和密码的机制。 幸运的是,ASP.NET 附带了许多成员资格提供程序,包括一个可使用 SQL Server 数据库的成员资格提供程序,还有一个使用 Active Directory的成员资格提供程序。 然而,如果没有符合您要求的提供程序,可直接创建一个自定义实现。
ASP.NET 授权
在您的用户通过身份验证后,请务必确保只有他们才能尝试调用服务。 在 ASP.NET 应用程序中,普通 WCF 服务和 WCF 数据服务均以.svc 文件表示。 本示例中,将在 IIS 中通过 ASP.NET 来承载服务,我将演示如何使用文件夹确保对服务的安全访问。
采用这种方式保护.svc 文件的安全有点令人迷惑不解,因为默认情况下,对此类文件的请求实际上会跳过大多数 ASP.NET 管道,从而绕过授权模块。 因此,为了能够利用许多 ASP.NET 功能,您必须启用 ASP.NET 兼容性模式。 在任何情况下,WCF 数据服务都会强制要求您启用它。 在您的配置文件内进行简单的切换即可完成任务:
<system.serviceModel> <serviceHostingEnvironment aspNetCompatibilityEnabled="true"/> </system.serviceModel> <system.web> <authorization> <deny users="?"/> </authorization> </system.web>
启用 ASP.NET 兼容性后,可防止未经身份验证的用户通过使用web.config 文件的授权部分(如上一代码段所示)进行访问。
当使用窗体身份验证时,开发人员必须认真考虑站点的哪些部分需要可访问,即使是对于未经身份验证的用户也是如此。 例如,如果所有部分均仅限于经过身份验证的用户访问,那么未经身份验证的用户将如何登录?
通常,创建一个支持您的基本授权要求的文件夹结构是最简单的方法。 在本示例中,我已经创建了一个包含 MyWcfService.svc 和 MyWcfDataService.svc 文件的“Secured”文件夹,并且已经部署了一个web.config 文件。 在图 2 中,您可以看到文件夹结构,上一代码段显示了web.config 文件的内容。
图 2 包含 Web.config 文件的 Secured 文件夹
请注意,应用程序的根必须允许匿名访问,否则用户无法到达登录页面。
对于使用 Windows 身份验证的站点,在这一点上可能稍微简单一些,因为身份验证是在用户到达应用程序内所含的资源之前进行的,因此不需要具体的登录页面。 实际上,使用此方法可以采用更加详细的方式限制对服务的访问,从而只允许特定的用户或角色组访问资源。 有关详细信息,请参阅“ASP.NET 授权”(msdn.microsoft.com/library/wce3kxhd)。
此示例在某种程度上实现了授权,但是对于大多数方案而言,单独的文件夹级授权过于粗糙,难以依赖。
WCF 服务中的授权
使用 PrincipalPermission 属性是要求 Microsoft .NET Framework 方法的调用程序限定于特定角色内的一种简单方法。 以下代码示例演示了如何在 WCF 中将其应用于 ServiceOperation,其中调用用户必须属于“OrderApprovers”角色:
[PrincipalPermission(SecurityAction.Demand, Role = "OrderApprovers")] public void ApproveOrder(int orderId) { OrderManag-er.ApproveOrder(orderId); }
这在使用 Windows 身份验证以利用现有设施创建 Active Directory组以便组织用户的应用程序中很容易实现。 借助使用窗体身份验证的应用程序,可以利用 ASP.NET 的另一项强大的基于提供程序的功能:RoleProviders。 此外,还有许多授权方法可用,如果这些方法均不适用,您可以实现自己的授权。
当然,即便是依据方法的授权也远远不足以满足您的所有安全需求,您可以选择在您的服务内编写程序代码(如下所示)。
Public void CancelOrder(int orderId) { // retrieve order using Entity Framework ObjectContext OrdersEntities entities = new OrdersEntities(); Order orderForProcessing = entities.Orders.Where(o => o.Id == orderId).First(); if (orderForProcessing.CreatedBy != Thread.CurrentPrincipal.Identity.Name) { throw new SecurityException( "Orders can only be canceled by the user who created them"); } OrderManager.CancelOrder(orderForProcessing); }
WCF 是一个具有高度可扩展性的平台,随着所有功能都集成到 WCF 中,有许多方法可以在您的服务中实现授权。 Dominick Baier 和 Christian Weyer 在 2008 年 10 月期的 MSDN 杂志 中详细讨论了大量可能方案。 文章“基于 WCF 服务中的授权”(msdn.microsoft.com/magazine/cc948343)甚至冒险尝试了基于声明的安全性(一种在您的应用程序中组织授权的结构化方法)。
WCF 数据服务中的授权
顾名思义,WCF 数据服务构建于 WCF 之上,以提供对数据源(通常大多数情况下可能是 LINQ-to-SQL 或 LINQ-to-Entity Framework 数据源)的基于 REST 的访问。 简而言之,这允许您使用映射到您的数据源公开的实体集的 URL 访问您的数据(实体集通常会映射到数据库的表中)。 这些实体集的权限可在服务代码隐藏文件内配置。下段代码显示了 MyWcfDataService.svc.cs 文件的内容。
Public class MyWcfDataService : DataService<SalesEntities> { // This method is called only once to initialize service-wide policies. Public static void InitializeService(IDataServiceConfiguration config) { config.SetEntitySetAccessRule("Orders", EntitySetRights.AllRead); config.SetEntitySetAccessRule("Products", EntitySetRights.AllRead | EntitySetRights.WriteAppend | EntitySetRights.WriteDelete); }}
在这里,我针对 Orders 实体集授予了“读取”权限,并且配置了 Products 实体集以允许完全读取、插入新记录和删除现有记录。
但是,由于 WCF 数据服务会自动基于此配置提供对您的数据的访问,您无法直接访问代码,因此显然无法实现任何特定授权逻辑。 WCF 数据服务支持允许开发人员在客户端和数据源之间实现逻辑的侦听器。 例如,可以指定一个筛选特定实体集结果的查询侦听器。下面代码中的示例显示了两个添加到 MyWcfDataService 类的查询侦听器。
[QueryInterceptor("Products")] Public Expression<Func<Product, bool>> OnQueryProducts() { String userName =ServiceSecurityContext.Current.PrimaryIdentity.Name; return product => product.CreatedBy == userName; } [QueryInterceptor("Orders")] Public Expression<Func<Comments, bool>> OnQueryOrders() { bool userInPrivateOrdersRole = Thread.CurrentPrincipal.IsInRole("PrivateOrders"); return order => !order.Private|| userInPowerUserRole; }
第一个侦听器被应用于 Products 实体集并确保了用户只能检索其自己创建的产品。 第二个侦听器确保了只有 PrivateOrders 角色的用户才能读取标记“Private”的订单。
同样,可以指定在插入、修改或删除某个实体之前运行的更改侦听器,如下所示:
[ChangeInterceptor("Products")] public void OnChangeProducts(Product product, UpdateOperations operations { if (product.CreatedBy != Thread.CurrentPrincipal.Identity.Name) { throw new DataServiceException( "Only products created by a user can be deleted by that user"); } }
乍一看,此代码示例中的 OnChangeProducts 更改侦听器似乎暴露了一个安全漏洞,因为实现依赖于从外部数据源(特别是“product”参数)传递的数据。 但是,当在 WCF 数据服务中删除一个实体时,仅会将一个实体关键字从客户端传递到服务器。 这意味着必须再从数据库中获取一次实体(在本例为 Product),因此实体本身可以信任。
但是,对于现有实体更新的情况(例如,当操作参数等于 UpdateOperations.Change 时),产品参数为客户端发送的反序列化实体,因此不可信。 客户端应用程序可能已被修改,以将此特定产品的 CreatedBy 属性指定为恶意用户自己的身份,从而提升篡夺者的权限。 这可能会允许不应修改某个产品的个人执行此操作。 为避免出现这种情况,建议您只基于实体关键字从受信任数据源重新获取原始实体,如下所示。
[ChangeInterceptor("Products")] Public void OnChangeProducts(Product product, UpdateOperations operations) { if (operations == UpdateOperations.Add) { product.CreatedBy = Thread.CurrentPrincipal.Identity.Name; } else if (operations == UpdateOperations.Change) { Product sourceProduct = this.CurrentDataSource.Products.Where(p => p.Id == product.Id).First(); if (sourceProduct.CreatedBy != Thread.CurrentPrincipal.Identity.Name) { throw new DataServiceException( "Only records created by a user can be modified by that user"); } } else if (operations == UpdateOperations.Delete && product.CreatedBy != Thread.CurrentPrincipal.Identity.Name) { Throw new DataServiceException( "Only records created by a user can be deleted by that user"); } }
由于此实现在很大程度上依赖于 Product 实体的 CreatedBy属性,因此从创建数据时起就以可靠的方式强制实施至关重要。以上代码还显示了如何通过重写客户端为“添加”操作传递的任何值来实现此目标。
请注意,按照示例目前的情况,UpdateOperations.Change 类型的处理操作不是问题。 MyWcfDataService.svc.cs中将服务配置为只允许对 Products 实体集执行 AllRead、WriteAppend(插入)和 WriteDelete 操作。 因此,永远无法为“更改”操作调用 ChangeInterceptor,因为服务会立即拒绝任何在此端点修改 Product 实体的请求。 要启用更新,MyWcfDataService.svc.cs中对 SetEntitySetAccessRule 的调用必须包括 WriteMerge、WriteReplace 或者两者均包括。
跨域身份验证
Silverlight 插件可进行跨域 HTTP 请求。 跨域调用是指对从其中下载 Silverlight 应用程序的域以外的其他域进行的 HTTP 请求。 能够进行此类调用在传统上被视为一种安全漏洞。 它允许恶意开发人员向另一个站点(例如,您的网上银行站点)发出请求,并自动转发与该域关联的任何 Cookie。 这可能会使攻击者能够访问相同浏览器进程内的另一个已登录的会话。
为此,站点必须通过部署一个跨域策略文件选择允许跨域调用。 这是一个说明允许哪些类型的跨域调用(例如,从哪些域到哪些 URL)的 XML 文件。 有关详细信息,请参阅“使服务跨域边界可用”(msdn.microsoft.com/library/cc197955(VS.95))。
当决定向跨域调用公开任何敏感信息时,您应该始终小心谨慎。 但是,如果您决定这是一个需要随身份验证一起支持的方案,请务必注意,基于 Cookie 的身份验证方法(如前面描述的窗体验证)不再适用。 您可以考虑利用消息凭据,其中用户名和密码被传递给服务器并在每次调用时进行验证。 WCF 通过 TransportWithMessageCredential 安全模式支持此操作。 有关详细信息,请参阅“如何:使用消息凭据来保障用于 Silverlight 应用程序的服务的安全”(msdn.microsoft.com/library/dd833059(VS.95))。
当然,此方法从身份验证过程中完全去除了 ASP.NET,因此难以一起利用 ASP.NET 授权(见上文讨论)。
保护您的 Silverlight XAP 文件的安全
担心 Silverlight 安全性的人常常会问:“怎样才能保护我的 XAP 文件?”有时候,这一疑问背后的动机是为了保护代码内所含的知识产权。 在这种情况下,您需要进行模糊处理,以使人更加难以理解您的代码。
另一个常见的动机是为了防止恶意用户询问代码和了解 Silverlight 应用程序的工作原理,从而为他们提供侵入您的服务的可能性。
我通常从以下两方面做出答复。
第一,尽管可以仅限经过身份验证并且获得授权的用户下载您的 Silverlight 应用程序(.xap 文件),但是没有任何理由相信这些用户所怀的恶意比未经过身份验证的用户要少。 在应用程序已被下载到客户端之后,绝对没有办法再阻止用户询问代码,以试图提升其自己的权限或将库转发给他人。 进行模糊处理可以使此过程稍微更加困难一些,却不足以确保您的应用程序安全。
第二,任何可以通过您的 Silverlight 应用程序合法调用服务的人也可以直接调用这些服务(例如,使用 Internet 浏览器和某些 JavaScript),记住这一点至关重要。 您根本无法阻止这一情况的发生,因此将您的安全性工作的重点放在支持您的服务上极为重要。 正确执行此操作,即使恶意用户可以从您的 Silverlight 应用程序的代码获得什么都无关紧要。 尽管如此,有些人仍希望确保只有经过身份验证的用户才能访问其.xap 文件。 这是可行的,但简便程度取决于您所使用的 IIS 的版本以及您选择的身份验证方法。
如果您使用的是 Windows 身份验证,那么您可以使用 IIS 目录安全性轻松地保护您的.xap 文件。 但如果您使用的是窗体身份验证,情况则会稍微复杂一些。 在这种情况下,要由 FormsAuthenticationModule 截获并验证任何请求附带的 Cookie,以及允许或拒绝访问请求的资源。
因为 FormsAuthenticationModule 是一个 ASP.NET 模块,因此请求必须通过 ASP.NET 管道传递,以便执行此检查。 在 IIS6 (Windows Server 2003)和先前的版本中,默认情况下,对.xap 文件的请求将不会通过 ASP.NET 传送。
尽管 IIS7 (Windows Server 2008)引入了集成管道,其允许所有请求均通过 ASP.NET 管道传送。 如果您可以部署到 IIS7 并使用在集成管道模式下运行的应用程序池,则保护您的.xap 文件安全的难度仅相当于保护您的.svc 文件的安全(如前面“ASP.NET 授权”部分所述)。 但是,如果您必须部署到 IIS6 或更早版本,您可能需要完成一些其他工作。
一种常用的方法涉及通过 ASP.NET 管道处理的另一个扩展流式处理组成您的.xap 文件的字节。 典型方式是通过 IHttpHandler 实现(在.ashx文件中)。 有关详细信息,请参阅“HTTP 处理程序简介”(msdn.microsoft.com/library/ms227675(VS.80))。
另一种方法是更改 IIS 的配置,以便通过 ASP.NET 管道发送.xap 文件。 但是,由于这需要对您的 IIS 配置进行重大更改,因此前一种方法更常用。
使用窗体身份验证需要考虑的另一个问题是登录屏幕。 正如本文前面所建议,如果您选择使用 ASP.NET Web 窗体,则不存在任何问题。 但是,如果您希望在 Silverlight 中编写登录屏幕,您将需要将应用程序拆分为几部分。 一部分(登录模块)应可用于未经身份验证的用户,另一部分(受保护的应用程序)应仅可用于经过身份验证的用户。
您可以采用两种方法:
- 拥有两个单独的 Silverlight 应用程序。 第一个包含登录对话框且位于站点未受保护的区域。 成功登录后,会重定向到指定站点受保护区域中的.xap file 文件的页面。
- 将您的应用程序拆分为两个或多个模块。 位于站点未受保护区域中的初始.xap 将执行身份验证过程。 如果成功,该.xap 文件随后将从受保护区域请求一个文件,该文件可动态加载到 Silverlight 应用程序中。 我最近发表了一篇如何实现此操作的博客文章(网址为:thejoyofcode.com/How_to_download_and_crack_a_Xap_in_Silverlight.aspx)。