共用方式為


本文章是由機器翻譯。

聯合身份標識

使用 WIF 實現 ASP.NET 被動身份驗證

Michele Leroux Leroux

聯合安全的目標是提供在域間建立信任關係的機制,這樣,使用者通過自己所屬域的身份驗證後,就獲得了授權可以訪問其他域的應用程式和服務。這使單一登入這樣的身份驗證方法成為可能,採用這種方法,無需針對多個應用程式和域為使用者配置和管理重複帳戶,從而顯著降低將應用程式擴展到受信任方的成本。

在聯合安全模型中,身份標識提供程式 (IdP) 執行身份驗證,並通過安全權杖服務 (STS) 頒發安全權杖。這些權杖斷言有關經過身份驗證的使用者的資訊:使用者身份標識和其他可能的資訊(包括角色和更細細微性的存取權限)。在聯合技術領域,這些資訊稱為聲明,基於聲明的存取控制是聯合安全模型的核心。在這一模型中,應用程式和服務根據來自受信任頒發機構 (STS) 的聲明,對特性和功能的訪問進行授權。

像 Windows Identity Foundation (WIF) 之類的平臺工具大大降低了支援這種聯合身份標識的難度。WIF 是一個身份標識模型框架,用於構建基於聲明的應用程式和服務,支援基於 SOAP(主動)和基於流覽器(被動)的聯合方案。在 MSDN 雜誌 2009 年 11 月刊中的文章“通過 WIF 實現基於聲明的授權”中,我重點介紹了如何在 Windows Communication Foundation (WCF) 中使用 WIF。在這篇文章中,我介紹了如何為 WCF 服務實現基於聲明的安全模型,以及如何遷移到聯合身份驗證。

在本後續文章中,我將重點介紹被動聯合。我將說明被動聯合的通信流程,介紹幾種在 ASP.NET 應用程式中支援聯合的方法,討論基於聲明的 ASP.NET 授權方法,然後介紹單一登入和單一登出方案。同時,我將介紹支援被動聯合方案的基礎 WIF 功能和元件。

被動聯合基礎

被動聯合方案基於 WS-Federation 規範。該規範規定了如何請求安全權杖,如何發佈和獲取聯合中繼資料文檔,從而簡化建立信任關係的過程。WS-Federation 還規定了單一登入和登出過程,以及其他聯合實現概念。

WS-Federation 討論了有關聯合的諸多細節,其中有一部分專門介紹基於流覽器的聯合,這種聯合依靠 HTTP GET 和 POST、流覽器重定向以及 Cookie 實現目標。

被動聯合消息傳遞的某些方面主要基於 WS-Trust 規範。例如,在請求 STS 的安全權杖時,被動聯合採用與流覽器相容的“請求安全權杖”(RST) 和“RST 回應”(RSTR) 形式。在被動聯合方案中,我將 RST 稱為登錄請求消息,將 RSTR 稱為登錄回應訊息。WS-Trust 規範側重于基於 SOAP(主動)的聯合,如 Windows 用戶端與 WCF 服務的聯合。

图 1 是一個簡單的被動聯合方案。

圖 1 簡單被動聯合方案

使用者在自己的域通過身份驗證,根據其角色獲得 Web 應用程式存取權限。此身份驗證方案中的參與者包括使用者(主體)、Web 流覽器(請求者)、ASP.NET 應用程式(依賴方,即 RP)、負責在使用者域中對使用者進行身份驗證的 IdP,以及屬於使用者域的 STS (IP-STS)。一系列的流覽器重定向操作可以確保使用者在訪問 RP 之前在自己的域內通過身份驗證。

使用者流覽至 RP 應用程式 (1),然後重定向到使用者的 IdP 進行身份驗證 (2)。如果使用者尚未在 IdP 經過身份驗證,IP-STS 可能進行質詢,或將使用者重定向到登錄頁面以收集憑據 (3)。使用者提供自己的憑據 (4),然後由 IP-STS (5) 進行身份驗證。此時,IP-STS 根據登錄請求頒發安全權杖,包含該權杖的登錄回應通過流覽器重定向發送到 RP (6)。RP 處理安全權杖,根據權杖攜帶的聲明授予存取權限 (7)。如果成功授權,則向使用者顯示其最初請求的頁面,並且返回會話 Cookie (8)。

使用 WIF 和 ASP.NET 實現此被動聯合方案只需要幾個步驟:

  1. 建立 RP 和 IdP (IP-STS) 之間的信任關係
  2. 對 ASP.NET 應用程式啟用被動聯合
  3. 實現授權檢查,以控制對應用程式功能的訪問。在後面幾節中,我將討論支援被動聯合的 WIF 功能,演示配置此簡單方案的步驟,然後討論此方案和其他方案在實踐中應注意的事項。

支援被動聯合的 WIF 功能

在討論實現之前,我們回顧一下 WIF 專用於在 ASP.NET 應用程式中支援聯合身份驗證的功能。首先,WIF 提供以下有用的 HTTP 模組:

  • WSFederationAuthenticationModule (FAM): 支援基於流覽器的聯合,重定向到適當的 STS 以進行身份驗證和權杖頒發,處理生成的登錄回應,將已頒發安全權杖創建為 ClaimsPrincipal 以進行授權。此模組還處理其他重要的聯合消息,如登出請求。
  • SessionAuthenticationModule (SAM): 通過生成包含 ClaimsPrincipal 的會話安全權杖,管理經過身份驗證的會話,將會話安全權杖寫入 Cookie,管理會話 Cookie 的生存期,如果 Cookie 已存在,則根據 Cookie 重建 ClaimsPrincipal。此模組還包含一個本地會話權杖緩存。
  • ClaimsAuthorizatonModule:提供一個可擴展點,以便安裝自訂 ClaimsAuthorizationManager,後者對於集中訪問檢查非常有用。
  • ClaimsPrincipalHttpModule:根據附加到請求執行緒的當前使用者身份創建 ClaimsPrincipal。另外,該模組還提供一個可擴展點來安裝自訂 ClaimsAuthenticationManager,後者用於自訂將附加到請求執行緒的 ClaimsPrincipal。

ClaimsPrincipalHttpModule 最適合不使用被動聯合的應用程式。您可將該模組看作一個有用的工具,在 ASP.NET 應用程式遷移到被動聯合之前,它可用來在應用程式內實現基於聲明的安全模型。我在以前的文章中討論過這一 WCF 方法。

其他三種模組通常一起用於被動聯合 — 不過 ClaimsAuthorizationModule 是可選的。图 2 演示這些核心模組如何構成請求管道,以及這些模組在典型的聯合身份驗證請求中的功能。

圖 2 被動聯合採用的 WIF 元件和 HTTP 模組

請注意圖 1 中的被動聯合流程,當使用者首先流覽至 RP 中的受保護頁面時 (1),對應用程式的訪問將被拒絕。FAM 處理還未授權的請求,生成登錄消息,然後將使用者重定向到 IP-STS (2)。IP-STS 驗證使用者身份 (3),生成登錄回應(包含頒發的安全權杖),然後重定向回 RP 應用程式 (4)。

FAM 處理登錄回應(確保回應包含經過身份驗證的使用者的有效安全權杖),根據登錄回應創建 ClaimsPrincipal (5)。這將為請求執行緒和 HttpContext 設置安全主體。然後,FAM 使用 SAM 將 ClaimsPrincipal 序列化為 HTTP Cookie (6),在流覽器會話期間用於後續請求。如果安裝了 ClaimsAuthorizationModule,該模組將調用經過配置的 ClaimsAuthorizationManager,以便在訪問請求的資源之前針對 ClaimsPrincipal 執行全域訪問檢查 (7)。

只要請求的資源存在,就可以實現存取控制,這一過程需要使用傳統的 ASP.NET 登錄控制項、IsInRole 檢查和其他查詢使用者聲明的自訂代碼 (8)。

進行後續請求時,將使用該會話權杖和 SAM 之前寫入的 Cookie (9)。這時,SAM 會驗證會話權杖,根據權杖重新創建 ClaimsPrincipal (10)。僅當請求是登錄回應、登出請求,或者請求被拒絕時(未提供會話權杖或者會話權杖已失效時,可能發生),FAM 才進行驗證。

除上述模組之外,被動聯合中還有兩個很有用的 ASP.NET 控制項:

  • FederatedPassiveSignIn 控制項:在以下情況下可替代 FAM:如果應用程式將所有未經授權的調用重定向到登錄頁面,而登錄頁面只在需要身份驗證時才承載此控制項。這裡假定使用者將與登錄過程進行交互,該過程在逐級身份驗證方案中很有用,在這類方案中,將提示使用者提供憑據(可能是除了原始登錄憑據之外,應用程式要求的其他憑據)。該控制項處理到 STS 的重定向,處理登錄回應,根據回應初始化 ClaimsPrincipal,並利用 FAM 和 SAM 公開的功能建立安全會話。
  • FederatedPassiveSignInStatus 控制項: 此控制項提供對話模式,供使用者登錄 RP 應用程式或從中登出,並且支援聯合登出。

图 3 演示採用 FederatedPassiveSignIn 控制項時通信流程是如何變化的。應用程式使用表單身份驗證來保護資源,並重定向到承載該控制項的登錄頁面 (1)。使用者按一下 FederatedPassiveSignIn 控制項(或自動重定向到該控制項),會觸發到 STS 的重定向 (2)。控制項頁面從 STS 接收回應,通過 FAM 和 SAM 處理登錄回應 (3),創建 ClaimsPrincipal,然後寫入會話 Cookie (4)。當使用者重定向到最初請求的頁面時 (5),SAM 驗證會話 Cookie,並為登錄請求創建 ClaimsPrincipal。此時,ClaimsAuthorizationModule 和該頁面可以執行授權檢查,如圖 2 所示。

圖 3 使用 FederatedPassive-SignIn 控制項的被動聯合

FAM 和 SAM 都通過適當的 SecurityTokenHandler 類型處理傳入權杖。登錄回應到達時,FAM 迴圈訪問 SecurityTokenHandlerCollection,查找正確的權杖處理常式來讀取 XML 權杖。在聯合方案中,這個權杖處理常式通常是 Saml11SecurityTokenHandler 或 Saml2SecurityTokenHandler,通過添加自訂權杖處理常式,您也可以採用其他權杖格式。SAM 使用 SessionSecurityTokenHandler 處理與會話 Cookie 相關聯的會話權杖。

對於被動聯合流程而言,某些身份標識模型配置設置十分重要,這些設置用於初始化 FAM、SAM 和 FederatedPassiveSignIn 控制項(當然,後者也會公開可在 Visual Studio 設計器中配置的屬性)。您可以以程式設計方式提供 Microsoft.IdentityModel.Configuration 命名空間的 ServiceConfiguration 類型的實例,也可以在 <microsoft.identityModel>節中提供聲明性配置。图 4 總結了身份標識模型設置,本文後面幾部分將對其中很多設置進行介紹。

圖 4 基本 <microsoft.identityModel>元素匯總

描述
<issuerNameRegistry> 指定受信任憑證授權的清單。此清單主要用於驗證權杖簽名,以便拒絕不受信任的證書所簽名的權杖。
<audienceUris> 指定傳入 SAML 權杖的有效訪問者 URI 的清單。可通過禁用此項允許所有 URI,但不建議這樣做。
<securityTokenHandlers> 自訂權杖處理常式的配置設置,或提供自訂權杖處理常式,用於控制對權杖進行驗證、身份驗證和序列化的方式。
<maximumClockSkew> 調整權杖和應用程式伺服器之間的允許時間差,以進行權杖驗證。預設偏差時間為 5 分鐘。
<certificateValidation> 控制證書的驗證方式。
<serviceCertificate> 提供一個服務證書,用於對傳入權杖進行解密。
<claimsAuthenticationManager> 提供一個自訂 ClaimsAuthenticationManager 類型,用於自訂或替換要附加到請求執行緒的 IClaimsPrincipal 類型。
<claimsAuthorizationManager> 提供一個自訂 ClaimsAuthorizationManager 類型,用於從中心元件控制對功能的訪問。
<federatedAuthentication> 提供特定于被動聯合的設置。

啟用被動聯合

WIF 簡化了為 ASP.NET 應用程式配置被動聯合的過程。STS 應提供聯合中繼資料(按照 WS-Federation 規範中的說明),WIF 提供聯合實用工具 (FedUtil.exe),該工具使用聯合中繼資料建立 RP 和 STS 之間的信任(以及對主動和被動聯合方案都適用的其他功能)。通過在 Visual Studio 中按右鍵 RP 專案,然後選擇“添加 STS 引用”,或者通過命令列,都可以調用 FedUtil。

您將使用 FedUtil 嚮導完成以下簡單步驟:

  • 在嚮導的第一頁上,確認嚮導要修改的設定檔和 RP 應用程式 URI。
  • 在第二頁上,指定將與 RP 建立信任關係的 STS 的聯合中繼資料 XML 文檔路徑。
  • 在第三頁上,提供用於解密權杖的證書。
  • 最後一頁是 STS 提供的聲明的清單,舉例來說,利用這個清單,您可以規劃存取控制決策。

完成上述嚮導步驟後,FedUtil 會修改專案,以便添加一個對 Microsoft.IdentityModel 程式集的引用。它還會修改 web.config,以便安裝 FAM 和 SAM 模組,並為這些模組提供身份標識模型配置設置。現在,應用程式就支援被動聯合了,會將未經授權的請求重定向到受信任的 STS。

這裡存在一個假設,即 STS 事先知道 RP(因此會為嘗試訪問 RP 的經過身份驗證的使用者頒發權杖),並且具有 RP 要求 STS 在加密權杖時使用的公開金鑰。這是對 ASP.NET 應用程式進行初步聯合設置的簡便方法。當然,這有助於理解如何在需要調整時從頭開始進行設置,以及如何更改嚮導啟用的基本設置。從現在起,我將重點介紹“從頭開始設置”方法。

如果不使用 FedUtil,您需要手動添加一個對 Microsoft.IdentityModel 程式集的引用,然後手動配置 FAM 和 SAM 模組以及必要的身份標識模型設置。HTTP 模組應添加到兩個部分:Internet Information Services (IIS) 6 的 system.web 和 IIS 7 的 system.webServer。假設應用程式承載于 IIS 7 中,WIF 模組的配置如下:

<modules>
  <!--other modules-->
  <add name="SessionAuthenticationModule" 
    type="Microsoft.IdentityModel.Web.SessionAuthenticationModule, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" 
    preCondition="managedHandler" />
  <add name="WSFederationAuthenticationModule" 
    type="Microsoft.IdentityModel.Web.WSFederationAuthenticationModule, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" 
    preCondition="managedHandler" />
</modules>

預設情況下,此配置僅保護副檔名進行了顯式映射,以便由 ASP.NET 管道處理的資源(.aspx、.asax 等等)。 為了保護聯合身份驗證的其他資源,應將這些副檔名映射到 IIS 中的 ASP.NET 管道,或在模組設置中,將 runAllManagedModulesForAllRequests 設置為 true(只適用于 IIS 7),如下所示:

<modules runAllManagedModulesForAllRequests="true">

為啟動 FAM,您還必須將 ASP.NET 身份驗證模式設置為 None,拒絕匿名使用者存取應用程式資源:

<authentication mode="None" />

<authorization>
  <deny users="?" />
</authorization>

兩種模組都使用圖 4 所示的身份標識模型配置設置,典型示例如圖 5 所示。這些設置大多數是由 FedUtil 生成的,但 certificateValidation 設置和 federatedAuthentication 中的某些設置除外。我通常推薦使用 PeerTrust 證書驗證模式,該模式需要將所有受信任證書(包括受信任頒發機構的證書)顯式添加到本地電腦的 TrustedPeople 存儲。

圖 5 被動聯合的身份標識模型配置

<microsoft.identityModel>
  <service>
    <issuerNameRegistry type="Microsoft.IdentityModel.Tokens.ConfigurationBasedIssuerNameRegistry, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35">
      <trustedIssuers>
        <add thumbprint="EF38A0A6D1274766093D3D78BFE4ECA77C62D5C3" 
          name="http://localhost:60768/STS/" />
      </trustedIssuers>
    </issuerNameRegistry>
    <certificateValidation certificateValidationMode="PeerTrust" 
      revocationMode="Online" trustedStoreLocation="LocalMachine"/>
    <audienceUris>
      <add value="http://localhost:50652/ClaimsAwareWebSite2/" />
    </audienceUris>
    <federatedAuthentication>
      <wsFederation passiveRedirectEnabled="true" 
        issuer="http://localhost:60768/STS/" 
        realm="http://localhost:50652/ClaimsAwareWebSite2/" 
        requireHttps="true" />
      <cookieHandler requireSsl="true" name="FedAuth"  
        hideFromScript="true" path="/ClaimsAwareWebSite2" />
    </federatedAuthentication>
    <serviceCertificate>
      <certificateReference x509FindType="FindByThumbprint" 
        findValue="8A90354199D284FEDCBCBF1BBA81BA82F80690F2" 
        storeLocation="LocalMachine" storeName="My" />
    </serviceCertificate>
  </service>
 </microsoft.identityModel>

被動聯合中通常需要 HTTPS/SSL 來保護頒發的持有者權杖免受中間人的攻擊,並且需要對會話 Cookie 使用 HTTPS/SSL。預設情況下,Cookie 對腳本是隱藏的,但它是一項重要設置,所以我在圖 5 中對它進行了設置。

至於 Cookie 的名稱和路徑,名稱預設為 FedAuth,路徑為應用程式目錄。為 Cookie 指定唯一名稱可能很有用,尤其是在解決方案中很多 RP 應用程式共用同一個域的情況下。相反,如果希望同一個域中多個應用程式共用 Cookie,您可以選擇指定一個通用路徑。

在採用 FAM 和 SAM 的被動聯合中,通常使用 FedUtil 配置 ASP.NET 應用程式,然後根據解決方案的要求調整相應設置。您也可以使用 PassiveFederationSignIn 控制項來代替 FAM,如圖 3 所示。您可以在 microsoft.identityModel 節中載入該控制項的設置,也可以直接設置控制項屬性。

如果希望將未經授權的請求重定向到登錄頁面(使用者可以在此通過按一下該控制項進行顯式登錄)而不是由 FAM 自動重定向到 STS,控制項方法會很有用。例如,如果使用者屬於多個身份標識提供程式(本地域),登錄頁面可能會提供一種機制,供使用者在重定向到 STS 之前指示其本地域。我稍後會討論本地域識別。

被動權杖頒發

如前所述,被動聯合使用 HTTP GET、POST 和流覽器重定向,實現 RP 與 STS 之間的通信。图 6 演示這一過程中登錄請求和登錄回應中涉及的主要請求參數。

圖 6 被動聯合請求中涉及的主要登錄請求和回應參數

STS 接收到登錄請求時,會根據它的已知 RP 域清單檢查 wtrealm 參數,驗證該 RP 是不是已知的。STS 可能已有該 RP 的知識,即加密權杖所需的證書,以及應包含在已頒發權杖中的聲明。如果該 RP 通過完整登錄請求提供可選的 wreq 參數,則可以指示需要哪些聲明,STS 可以選擇根據這個清單,也可以根據經過身份驗證的使用者自主決定對哪些聲明授權。

圖 1 所示的簡單聯合方案中,只有一個 RP 和一個 IP-STS 負責使用者身份驗證。如果 IP-STS 針對 Windows 域進行使用者身份驗證,它可能頒發角色聲明,如管理員、使用者或來賓。前提是這些角色對於 RP 授權是有意義的。在下一部分中,我假設這些角色是有意義的,然後討論授權方法。在這之後,我將討論 RP 聲明轉換,根據需要將 STS 聲明轉換為對於授權更加有用的內容。

基於聲明的授權

我在以前的文章中提到過,.NET Framework 中基於角色的安全機制要求安全主體附加到每個執行緒。安全主體(基於 IPrincipal)在 IIdentity 實現中包裝經過身份驗證的使用者的標識。WIF 根據 IClaimsPrincipal 和 IClaimsIdentity(最終由 IPrincipal 和 IIdentity 派生)提供 ClaimsPrincipal 和 ClaimsIdentity 類型。FAM 處理登錄回應時,會為已頒發安全權杖創建 ClaimsPrincipal。同樣,SAM 也為會話 Cookie 創建 ClaimsPrincipal。此 ClaimsPrincipal 是 ASP.NET 應用程式進行 WIF 授權的核心。

您可以使用以下任何方法進行授權:

  • 使用位置特定的授權設置限制對目錄或各應用程式資源的訪問。
  • 使用 ASP.NET 登錄控制項(如 LoginView 控制項)控制對功能的訪問。
  • 使用 ClaimsPrincipal 執行動態 IsInRole 檢查(如動態隱藏或顯示 UI 元素)。
  • 使用 PrincipalPermission 類型或 PrincipalPermissionAttribute(如果聲明性許可權請求看起來適用于某種特定方法)執行動態許可權請求。
  • 提供自訂 ClaimsAuthorizationManager,將存取權限檢查集中在一個元件中,即使在載入請求的資源之前也不例外。

在上述方法中,前三個需要使用 ClaimsPrincipal 類型公開的 IsInRole 方法。您必須選擇一個適合於 IsInRole 檢查的角色聲明類型,以便使用正確的聲明控制訪問。WIF 的預設角色聲明類型為:

https://schemas.microsoft.com/ws/2008/06/identity/claims/role

如果 ClaimsPrincipal 包含定義的聲明,則角色聲明類型將與預設值匹配。 稍後,我將討論聲明轉換上下文中的許可權聲明。 使用這些聲明時,應將許可權聲明類型指定為角色聲明類型,以便使 IsInRole 生效。

您可以使用 web.config 檔對訪問特定頁面或特定目錄進行全域控制。 在應用程式根目錄中提供一個位置標記,指定要保護的路徑,允許可接受的角色進行訪問,拒絕所有其他使用者的訪問。 下麵的代碼只允許 Administrators 訪問 AdminOnly 目錄下的檔:

<location path="AdminOnly">
  <system.web>
    <authorization>
      <allow roles="Administrators" />
      <deny users="*"/>
    </authorization>
  </system.web>
</location>

此外,您還可以在任何子目錄中放置 web.config,用於指定授權規則。 將下麵的配置放置到 AdminOnly 目錄中,結果是一樣的:

<configuration>
  <system.web>
    <authorization >
      <allow roles="Administrators" />
      <deny users="*"/>
    </authorization>
  </system.web>
</configuration>

要動態隱藏或顯示 UI 元件或控制對頁面內功能的訪問,可以使用基於角色的控制項功能(如 LoginView)。 但是,大多數開發人員更願意在頁面載入過程中顯式設置控制項的存取控制屬性,以獲得更精細的控制。 為此,您可以調用 ClaimsPrincipal 公開的 IsInRole 方法。 您可以通過 Thread.CurrentPrincipal 靜態屬性訪問當前主體,如下所示:

if (!Thread.CurrentPrincipal.IsInRole("Administrators"))
  throw new SecurityException("Access is denied.");

除了在運行時顯式檢查 IsInRole 之外,還可以使用 PrincipalPermission 類型寫入傳統的基於角色的許可權要求。 還可以使用所要求的角色聲明(第二個構造函數參數)初始化該類型,當調用 Demand 時,將調用當前主體的 IsInRole 方法。 如果找不到該聲明,則引發異常:

PrincipalPermission p = 
  new PrincipalPermission("", "Administrators");
p.Demand();

如果要在相應角色不存在時拒絕請求並引發異常,此方法很有用。

集中處理所有被請求資源的通用許可權檢查也是很有用的。 有時,如果有存取控制策略(如存儲在資料庫中的規則),則可以使用中心元件讀取這些規則,從而控制對特性和功能的訪問。 為此,WIF 提供了一個可以擴展的 ClaimsAuthorizationManager 元件。 我以前的文章中提到過,您可以在身份標識模型節配置這種自訂群組件類型:

<microsoft.identityModel>
  <service>
    <!--other settings-->
    <claimsAuthorizationManager 
      type="CustomClaimsAuthorizationManager"/>
  </service>
</microsoft.identityModel>

图 7 演示一個自訂的 ClaimsAuthorizationManager,它驗證名稱聲明是否存在,AdminsOnly 目錄內的被請求資源是否需要 Administrators 角色聲明。

圖 7 自訂 ClaimsAuthorizationManager 實現

public class CustomClaimsAuthorizationManager: 
  ClaimsAuthorizationManager {

  public CustomClaimsAuthorizationManager()
  { }

  public override bool CheckAccess(
    AuthorizationContext context) {

    ClaimsIdentity claimsIdentity = 
      context.Principal.Identity as ClaimsIdentity;
    if (claimsIdentity.Claims.Where(
      x => x.ClaimType == ClaimTypes.Name).Count() <= 0)
      throw new SecurityException("Access is denied.");
        
    IEnumerable<Claim> resourceClaims = 
      context.Resource.Where(x=>x.ClaimType==ClaimTypes.Name);
    if (resourceClaims.Count() > 0) {
      foreach (Claim c in resourceClaims) {
        if (c.Value.Contains("\AdminOnly") && 
          !context.Principal.IsInRole("Administrators"))
          throw new SecurityException("Access is denied.");
      }
    }

    return true;
  }
}

CustomClaimsAuthorizationManager 通過重寫 CheckAccess 實現這一功能。 此方法提供一個 AuthorizationContext 參數,該參數提供有關如下內容的資訊:請求操作(對於被動聯合來說,是 HTTP 謂詞,如 GET 或 POST)、請求的資源(一個 URI)和尚未附加到請求執行緒的 ClaimsPrincipal。

聲明轉換

通常,IP-STS 發出的聲明對描述經過身份驗證��使用者是有用的,但與 RP 的授權要求無關。 IdP 的任務不是瞭解每個 RP 的授權需要哪種類型的角色、許可權或其他更細細微性的項, 而是授權給身份標識提供程式域有關的聲明,或 IdP 可針對經過身份驗證的使用者斷言的聲明。

因此,RP 可能需要將聲明從 IP-STS 轉換為與授權更加相關的項。 這表明 RP 可以將使用者標識映射(可能按使用者名稱或 UPN)到 RP 聲明集。 假設 IP-STS 授權給預設角色聲明,圖 8 列出了 RP 按照每個傳入角色聲明發出的可能許可權聲明集。 許可權聲明類型可以是 RP 定義的自訂聲明類型,如:

urn:ClaimsAwareWebSite/2010/01/claims/permission

自訂 ClaimsAuthenticationManager 很適合轉換傳入 IP-STS 聲明。 將以下代碼添加到 microsoft.identityModel 節,可以安裝自訂 ClaimsAuthenticationManager:

<microsoft.identityModel>
  <service>
    <!--other settings-->
    <claimsAuthenticationManager 
      type="CustomClaimsAuthenticationManager"/>
  </service>
</microsoft.identityModel>

图 9 是一個示例 CustomClaimsAuthenticationManager,它將 IP-STS 授權的傳入角色聲明轉換為與 RP 相關的許可權聲明。

圖 8 將角色聲明轉換為 RP 的許可權聲明

角色聲明 許可權聲明
Administrators Create、Read、Update、Delete
使用者 Create、Read、Update
Guest Read

圖 9 RP 自訂聲明轉換

public class CustomClaimsAuthenticationManager: 
  ClaimsAuthenticationManager {

  public CustomClaimsAuthenticationManager() { }

  public override IClaimsPrincipal Authenticate(
    string resourceName, IClaimsPrincipal incomingPrincipal) {

    IClaimsPrincipal cp = incomingPrincipal;
    ClaimsIdentityCollection claimsIds = 
      new ClaimsIdentityCollection();

    if (incomingPrincipal != null && 
      incomingPrincipal.Identity.IsAuthenticated == true) {

      ClaimsIdentity newClaimsId = new ClaimsIdentity(
        "CustomClaimsAuthenticationManager", ClaimTypes.Name, 
        "urn:ClaimsAwareWebSite/2010/01/claims/permission");

      ClaimsIdentity claimsId = 
        incomingPrincipal.Identity as ClaimsIdentity;
      foreach (Claim c in claimsId.Claims)
        newClaimsId.Claims.Add(new Claim(
          c.ClaimType, c.Value, c.ValueType, 
          "CustomClaimsAuthenticationManager", c.Issuer));

      if (incomingPrincipal.IsInRole("Administrators")) {
        newClaimsId.Claims.Add(new Claim(
          "urn:ClaimsAwareWebSite/2010/01/claims/permission", 
          "Create"));
        newClaimsId.Claims.Add(new Claim(
          "urn:ClaimsAwareWebSite/2010/01/claims/permission", 
          "Read"));
        newClaimsId.Claims.Add(new Claim(
          "urn:ClaimsAwareWebSite/2010/01/claims/permission", 
          "Update"));
        newClaimsId.Claims.Add(new Claim(
          "urn:ClaimsAwareWebSite/2010/01/claims/permission", 
          "Delete"));
      }

      else if (incomingPrincipal.IsInRole("Users")) {
        newClaimsId.Claims.Add(new Claim(
          "urn:ClaimsAwareWebSite/2010/01/claims/permission", 
          "Create"));
        newClaimsId.Claims.Add(new Claim(
          "urn:ClaimsAwareWebSite/2010/01/claims/permission", 
          "Read"));
        newClaimsId.Claims.Add(new Claim(
          "urn:ClaimsAwareWebSite/2010/01/claims/permission", 
          "Update"));
      }

      else {
        newClaimsId.Claims.Add(new Claim(
          "urn:ClaimsAwareWebSite/2010/01/claims/permission", 
          "Read"));
      }

      claimsIds.Add(newClaimsId);
      cp = new ClaimsPrincipal(claimsIds);
    }

    return cp;
  }
}

為實現 IsInRole 檢查(如前所述),您必須提供許可權聲明類型作為角色聲明類型。 在圖 9 中,這是在構造 ClaimsIdentity 時指定的,因為 RP 要創建 ClaimsIdentity。

如果傳入 SAML 權杖是聲明的源,則可以向 SecurityTokenHandler 提供角色聲明類型。 下麵的代碼說明如何以聲明方式配置 Saml11SecurityTokenHandler,從而將許可權聲明類型用作角色聲明類型:

<microsoft.identityModel>
  <service>
    <!--other settings-->
    <securityTokenHandlers>
      <remove type="Microsoft.IdentityModel.Tokens.Saml11.Saml11SecurityTokenHandler, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
      <add type="Microsoft.IdentityModel.Tokens.Saml11.Saml11SecurityTokenHandler, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35">
        <samlSecurityTokenRequirement >
          <roleClaimType 
            value= "urn:ClaimsAwareWebSite/2010/01/claims/permission"/>
        </samlSecurityTokenRequirement>
      </add>
    </securityTokenHandlers>
  </service>
</microsoft.identityModel>

SAML 權杖處理常式包含 samlSecurityTokenRequirement 節,在該節中可以提供名稱和角色聲明類型的設置,以及與證書驗證和 Windows 權杖相關的其他設置。

本地域識別

在前文中,我已重點介紹了包含單個 IP-STS 的簡單聯合方案。 假設 RP 始終重定向到特定 IP-STS 對使用者進行身份驗證。

不過,在聯合技術領域,RP 可以信任來自多個域的多個權杖頒發機構。 這種情況下,出現了新的挑戰,因為 RP 必須確定應由哪個 IP-STS 對請求訪問資源的使用者進行身份驗證。 通過身份驗證確定的使用者所屬的域是使用者的主域,因此,這一過程稱為主域識別。

應用程式可以使用很多機制進行主域識別:

  • 在本示例中,主域是事先已知的,因此,請求始終重定向到特定 IP-STS。
  • 使用者可以從其他入口流覽至 RP,這樣可以提供一個查詢字串,從該入口指示使用者的主域。
  • RP 可以要求使用者登錄針對每個主域的特定入口頁。 登錄頁面可以假設一個特定的主域。
  • RP 可以根據請求的 IP 位址或其他某種試探法確定主域。
  • 如果 RP 無法通過上述方法確定主域,則可以顯示一個 UI,供使用者選擇主域或提供資訊説明 RP 確定主域。
  • 如果 RP 支援資訊卡,則所選卡可使用主動聯合將身份驗證驅動至相應的主域。
  • WS-Federation 簡要規定了如何實現主域識別服務,但沒有對此定義完備的規範。

無論如何識別主域,目標都是重定向使用者,從而使用正確的 IP-STS 進行身份驗證。 有幾種可能的方案。 一個方案是,RP 需要動態設置頒發機構 URI,以便將登錄請求發送到正確的 IP-STS。 這種情況下,RP 必須在 trustedIssuers 節中列出所有受信任的 IP-STS,如:

<trustedIssuers>
  <add thumbprint="6b887123330ae8d26c3e2ea3bb7a489fd609a076" 
    name="IP1" />
  <add thumbprint="d5bf17e2bf84cf2b35a86ea967ebab838d3d0747" 
    name="IP2" />
</trustedIssuers>

另外,您也可以重寫由 FAM 公開的 RedirectingToIdentityProvider 事件,並使用相關試探法為 STS 確定正確的 URI。 為此,可將以下代碼添加到 Global.asax 實現中:

void WSFederationAuthenticationModule_RedirectingToIdentityProvider(
  object sender, RedirectingToIdentityProviderEventArgs e) {
  if (e.SignInRequestMessage.RequestUrl.Contains(
    "IP1RealmEntry.aspx")) {
    e.SignInRequestMessage.BaseUri = 
      new Uri("https://localhost/IP1/STS/Default.aspx");
  }
  else if (e.SignInRequestMessage.RequestUrl.Contains(
    "IP2RealmEntry.aspx")) {
    e.SignInRequestMessage.BaseUri = new Uri(
       "https://localhost/IP2/STS/Default.aspx");
  }
}

其他方案需要將主域參數 (whr) 隨登錄請求一起傳遞給主 STS��� 例如,RP 可能有負責聲明轉換的資源 STS(R-STS 或 RP-STS)。 RP-STS 不對使用者進行身份驗證(它不是 IdP),但它與一個或多個其他 IdP 有信任關係。

RP 與 RP-STS 有信任關係,始終認可由 RP-STS 頒發的權杖。 RP-STS 負責將每個請求重定向到正確的 IdP。 RP-STS 可以確定要重定向到的正確 IP-STS(如前面的代碼所示),不過 RP 還可以提供有關主域的資訊,將主域參數中的這些資訊傳遞到 RP-STS。 這種情況下,RP 動態設置主域參數:

void WSFederationAuthenticationModule_RedirectingToIdentityProvider(
  object sender, RedirectingToIdentityProviderEventArgs e) {
  if (e.SignInRequestMessage.RequestUrl.Contains(
    "IP1RealmEntry.aspx")) {
    e.SignInRequestMessage.HomeRealm = 
      "https://localhost/IP1/STS/Default.aspx";
  }
  else if (e.SignInRequestMessage.RequestUrl.Contains(
    "IP2RealmEntry.aspx")) {
    e.SignInRequestMessage.HomeRealm = 
      "https://localhost/IP2/STS/Default.aspx";
  }
}

RP-STS 使用此參數重定向到正確的 IP-STS,然後將聲明由 IP-STS 轉換為與 RP 相關的聲明。

單一登入和單一登出

單一登入和單一登出是聯合身份驗證的重要組成部分。通過單一登入功能,使用者只要經過一次身份驗證,就可以訪問多個 RP 應用程式。顧名思義,單一登出是通過一次請求就從所有 RP 應用程式和所有相關 STS 鏈中登出。

圖 1 所示的簡單聯合方案中,使用者進行 IP-STS 身份驗證,並根據頒發的安全權杖獲得 RP 授權。身份驗證結束後,使用者有一個 STS 會話 Cookie 和一個 RP 會話 Cookie。現在,如果該使用者流覽至其他 RP,則會重定向到 IP-STS 進行身份驗證(假設兩個 RP 應用程式都信任同一個 IP-STS)。因為該使用者已經有了一個 IP-STS 會話,所以該 STS 將為第二個 RP 頒發一個權杖,而不提示需要憑據。現在,使用者獲得了訪問第二個 RP 的許可權,並且有第二個 RP 的新會話 Cookie。

前面已經討論過,WIF 提供 SAM 為經過身份驗證的使用者寫出會話 Cookie。預設情況下,此會話 Cookie 頒發給域的相對應用程式位址,其基本名稱是 FedAuth。因為聯合會話 Cookie 可能較大,所以權杖通常拆分為兩個(或更多)Cookie:FedAuth、FedAuth1 等等。

如果同一個域中承載了多個應用程式,在聯合方案中,預設行為是,流覽器針對每個 RP 都有一個 FedAuth Cookie(請參閱圖 10)。流覽器只為請求發送與域和路徑相關的 Cookie。

圖 10 與每個 RP 和 STS 關聯的會話 Cookie

通常情況下,都可以使用此預設行為,不過,有時需要針對每個會話 Cookie 提供特定于每個應用程式的唯一名稱,尤其是這些應用程式承載于同一個域時。同一個域中的多個應用程式也可能共用一個會話 Cookie,這種情況下,可以將 Cookie 路徑設置為“/”。

如果會話 Cookie 到期,流覽器會從緩存中將它刪除,使用者將再次被重定向到 STS 進行身份驗證。另外,如果與會話 Cookie 關聯的已頒發權杖已到期,WIF 將重定向到 STS 申請新的權杖。

登出更為明確,通常由使用者主動執行。單一登出是 WS-Federation 規範的可選功能,該功能建議 STS 還應通知它頒發過登出請求權杖的其他 RP 應用程式。這樣,會從使用者在單一登入會話期間流覽到的所有應用程式中刪除會話 Cookie。在更複雜的包含多個 STS 的方案中,接收到登出請求的主 STS 還應通知其他 STS 執行相同的操作。

為便於討論,我將著重介紹為實現聯合單一登出需要對 RP 執行的操作。您可以將 FederatedPassiveSignInStatus 控制項添加到任何需要支援登錄或登出的頁面,該控制項將自動指示其狀態。登錄之後,該控制項會顯示一個用於登出的連結、按鈕或圖像。

按一下該控制項時,它將根據 SignOutAction 屬性處理登出,該屬性可以是 Refresh、Redirect、RedirectToLoginPage 或 FederatedPassiveSignOut。如果是前三個屬性,則會刪除應用程式的會話 Cookie,但不會通知 STS 有關登出請求的資訊。如果選擇 FederatedPassiveSignOut 設置,該控制項將調用 WSFederationAuthenticationModule 的 SignOut。這可以確保從應用程式中刪除聯合會話 Cookie。另外,登出請求將發送到 STS:

GET https://localhost/IP1/STS?wa=wsignout1.0

如果不使用 FederatedPassiveSignInStatus 控制項,則可以直接調用 WSFederationAuthenticationModule.SignOut,以觸發將登出請求重定向到 STS 的操作。

單一登出意味著使用者從使用聯合身份標識登錄的所有應用程式登出。 如果 STS 支援此功能,則應保存會話期間使用者登錄的 RP 應用程式清單,在請求聯合登出時向每個 RP 發出清除請求:

GET https://localhost/ClaimsAwareWebSite?wa=wsignoutcleanup1.0

在更複雜的方案中,同樣的清除請求應發送到聯合會話涉及的所有其他 STS。為此,STS 必須事先知道每個 RP 和 STS 的清除 URI。要支援單一登出,RP 應能夠處理這些清除請求。FAM 和 FederatedPassiveSignInStatus 控制項都支援此功能。如果使用 FAM,可將清除請求發送到 RP 的任一 URI,FAM 將處理該請求,清除所有會話 Cookie。如果使用 FederatedPassiveSignInStatus 控制項,則必須將清除請求發送到包含該控制項的頁面。

事實上,除了建議的查詢字串和通信流程,WS-Federation 規範未詳細規定如何實現單一登出和清除行為。保證單一登出對所有聯合合作夥伴有效,這並不容易,但如果您擁有這樣的環境,並希望達到此目標,它是確實可行的。

Michele Leroux Bustamante 是 IDesign (idesign.net) 的首席架構師,也是 BiTKOO (bitkoo.com) 的首席安全架構師。她還是 Microsoft 聖地牙哥區域總監和Microsoft 互聯繫統 MVP。歡迎訪問她的博客 dasblonde.net

衷心感謝以下技術專家對本文的審閱:Govind Ramanathan