保護您的 Silverlight 應用程序的安全
作為一名 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)。