Partager via


使用 SAML 宣告、SharePoint、WCF、對 Windows Token 服務的宣告及限制委派存取 SQL Server

英文原文已於 2011 年 8 月 7 日星期日發佈

這可能是我有史以來所寫過最長的一篇專題文章了,因為我希望能夠將所討論到的所有相關技術全都含括在內。這塊領域最近有很多的討論,大多是有關如何讓 SAML 宣告使用者和 Windows 內容,能存取一些其他的應用程式。SharePoint 2010 雖然有限度地支援使用「對 Windows Token 服務的宣告」(以下簡稱為 c2wts),但對於 Windows 宣告使用者也只提供小部分的服務應用程式。很多人常會問,為什麼 SharePoint 2010 不能並用 SAML 宣告使用者與有效的 UPN 宣告?但其實真正的原因並不在技術層面。因此夾在驗證類型間的限制與可以使用 SharePoint 2010 之服務應用程式限制之間的您,應該心知肚明您必須找出一種方法作為基礎的 Windows 帳戶,由其連接 SAML 使用者與其他應用程式。我希望這篇文章能夠有助於您了解這個作法的基本概念。

這種情況的基本作法是建立 WCF 服務應用程式,處理所有來自於其他應用程式 (在這裡是指 SQA Server) 使用者的資料要求,因此,我會使用造訪 SharePoint 網站的 SAML 使用者身分,並以該 SAML 使用者的 Windows 帳戶發出要求,從 SQL Server 擷取資料。注意: 即使本文是以 SAML 宣告使用者為主,但相同的方法也可套用到 Windows 宣告使用者身上。Windows 使用者在登入時,預設會得到 UPN 宣告。整個程序的流程大致如下:

設定 SQL Server

讓我們先從 SQL Server 開始。在我的設定之中,SQL Server 會在 “SQL2” 伺服器上執行。SQL 服務本身會以網路服務形式執行,換言之也就是我不需要為 SQL 服務建立 SPN。倘若 SQL 服務是以網域帳戶形式執行,我就必須為 MSSQLSvc 的服務帳戶建立 SPN。在此設定中,我將使用既有的 Northwinds 資料庫擷取資料。為能清楚地說明發出要求之使用者的身分,我將「十項最貴的產品」預存程序修改成如下所示:

 

CREATE procedure [dbo].[TenProductsAndUser] AS

SET ROWCOUNT 10

SELECT Products.ProductName AS TenMostExpensiveProducts, Products.UnitPrice, SYSTEM_USER As CurrentUser

FROM Products

ORDER BY Products.UnitPrice DESC

 

請特別注意我在 SELECT 陳述式中加入了 SYSTEM_USER,目的是在資料欄內傳回目前的使用者,亦即,當我執行的查詢傳回結果時,會在我的資料表中顯示一個資料欄,並在資料欄中包含目前使用者的名稱,以便於我能夠很快地知道,查詢是不是以目前使用者的身分執行。在此特定的設定中,我授與三位 Windows 使用者執行此預存程序的權限;除了這三位使用者之外,無人可以執行此作業 (在最後的結果中也會使用這個範例)。

建立 WCF 服務應用程式

接下來是建立 WCF 服務,以從 SQL 擷取資料。我使用了我先前在 CASI Kit 文章第二部分 (https://blogs.msdn.com/b/sharepoint_cht/archive/2010/12/17/azure-sharepoint-2.aspx) 中所述的程序執行此作業。這項作業的目的在建立 SharePoint 伺服器陣列與 WCF 應用程式之間的信任。這是必要動作,否則將無法取得發出要求之使用者的宣告。您應該不只會想要以參數形式傳送 UPN 宣告,舉例來說,如果只是以不同的 UPN 宣告值進行傳送,雲端上的任何人都有可能可以假冒其他使用者的身分。但是一旦正確地建立 WCF 與 SharePoint 之間的信任之後,我就可以撰寫執行下列作業的方法:

  • 擷取 UPN 宣告
  • 使用 c2wts 模擬使用者
  • 以該使用者的身分從 SQL 擷取資料

 

以下是我用於執行這項程序的程式碼:

 

//the following added for this code sample:

using Microsoft.IdentityModel;

using Microsoft.IdentityModel.Claims;

using System.Data;

using System.Data.SqlClient;

using System.Security.Principal;

using Microsoft.IdentityModel.WindowsTokenService;

using System.ServiceModel.Security;

 

 

public DataSet GetProducts()

{

 

   DataSet ds = null;

 

   try

   {

       string conStr = "Data Source=SQL2;Initial Catalog=

       Northwind;Integrated Security=True;";

 

       //ask for the current claims identity

       IClaimsIdentity ci =

          System.Threading.Thread.CurrentPrincipal.Identity as IClaimsIdentity;

 

       //make sure the request had a claims identity attached to it

       if (ci != null)

       {

          //see if there are claims present before running through this

          if (ci.Claims.Count > 0)

          {

              //look for the UPN claim

              var eClaim = from Microsoft.IdentityModel.Claims.Claim c in ci.Claims

              where c.ClaimType == System.IdentityModel.Claims.ClaimTypes.Upn

              select c;

 

              //if we got a match, then get the value for login

              if (eClaim.Count() > 0)

              {

                 //get the upn claim value

                 string upn = eClaim.First().Value;

 

                 //create the WindowsIdentity for impersonation

                 WindowsIdentity wid = null;

 

                 try

                 {

                     wid = S4UClient.UpnLogon(upn);

                 }

                 catch (SecurityAccessDeniedException adEx)

                 {

                           Debug.WriteLine("Could not map the upn claim to " +

                     "a valid windows identity: " + adEx.Message);

                 }

 

                 //see if we were able to successfully login

                 if (wid != null)

                 {

                        using (WindowsImpersonationContext ctx = wid.Impersonate())

                    {

                       //request the data from SQL Server

                        using (SqlConnection cn = new SqlConnection(conStr))

                        {

                           ds = new DataSet();

                           SqlDataAdapter da =

                               new SqlDataAdapter("TenProductsAndUser", cn);

                           da.SelectCommand.CommandType =

                               CommandType.StoredProcedure;

                           da.Fill(ds);

                        }

                     }

                 }

              }

          }

       }

   }

   catch (Exception ex)

   {

       Debug.WriteLine(ex.Message);

   }

 

   return ds;

}

 

這組程式碼不是太複雜,所以在這兒我簡單地說明一下。首先是我先確定是否具備有效的宣告身分識別內容,確認之後接著再查詢宣告清單,以尋找 UPN 宣告。當我找到 UPN 宣告之後,我會從其中擷取值,然後呼叫 c2wts,以該使用者的身分執行 S4U 登入。倘若登入成功,將會傳回 WindowsIdentity。我接著使用該 WindowsIdentity 建立模擬內容,並在模擬該使用者的期間建立對 SQL Server 的連線,以擷取資料。以下是一些您可能想要使用的疑難排解秘訣:

  1. 您若未設定 c2wts 供您的應用程式集區使用,則在外部擷取區塊中會陷於「WTS0003: 呼叫者無權存取此服務」一類的錯誤。我將在後文對此進行詳細說明,並提供設定 c2wts 的連結。
  2. 若未正確地設定 Kerberos 限制委派,則當您嘗試使用 da.Fill(ds); 程式碼行執行預存程序時,它會傳回例外,表示匿名使用者無權執行此預存程序。在下段文章中,我將就此處的設定說明如何設定限制委派。

設定 C2WTS

c2wts 預設 a) 要以手動啟動,同時 b) 不允許任何人使用。我變更了它的設定,使之 a) 能夠自動啟動,並 b) 授權我的 WCF 服務應用程式集區可以使用它。我的建議是與其花很多時間仔細鑽研這項授權的設定方法,不如閱讀下列這篇文章 (設定資訊位在文章的結尾): https://msdn.microsoft.com/zh-tw/library/ee517258.aspx,文章中的資訊就綽綽有餘了。此外,您如需了解 c2wts 的幕後資訊,建議您閱讀下列這篇文章: https://msdn.microsoft.com/zh-tw/library/ee517278.aspx.

 

注意: 在最後這篇文章中有一大錯誤,就是建議使用者執行此程式碼 sc config c2wts depend=cryptosvc 建立 c2wts 的相依項目。此舉萬萬不可! 這行程式碼有打錯字,且 “cryptosvc” 並非有效的服務名稱,至少在 Windows Server 2008 R2 上不是有效的名稱。如果您照做了,c2wts 將表示相依項目已標示為刪除或找不到而無法再啟動。我自己曾經陷入此泥沼,後來將相依項目變更為 iisadmin (此為邏輯項目,因為在我的設定中,最低限度必須執行 WCF 主機,我才能使用 c2wts) 才得脫困。

設定 Kerberos 限制委派

在大家還沒被這篇文章嚇得打退堂鼓之前,我想說:

  1. 我不會過於仔細地告訴大家如何讓 Kerberos 限制委派成功地運作。坊間有很多有關這項主題的書籍可供參考。
  2. 就我個人的經驗,當我寫這篇文章時這部分的運作真的十分順利。

 

現在就讓我們逐一檢視委派所需要的項目。一如前文所述,我的 SQL Server 服務會以網路服務形式執行,因此我無須執行任何設定工作。其次因為我的 WCF 應用程式集區會以網域帳戶 vbtoys\portal 執行,因此我必須執行下列兩個動作:

  1. 使用將獲委派之伺服器的 NetBIOS 名稱與完整名稱,為帳戶建立 HTTP SPN。在我的設定中,WCF 伺服器稱為 AZ1,因此我建立了兩個 SPN,如下所示:
    1. setspn -A HTTP/az1 vbtoys\portal
    2. setspn -A HTTP/az1.vbtoys.com vbtoys\portal
  2. 我必須設定讓伺服器 "SQL2" 上所執行的 SQL Server 服務信任我的帳戶,才能夠運作 Kerberos 限制委派,因此,我在我的網域控制站開啟了 [Active Directory 使用者及電腦]。我在 vbtoys\portal 使用者上按了兩下,然後又按一下 [委派] (Delegation) 索引標籤以設定此信任關係。我設定為僅信任特定服務的委派,並可使用任何一種的驗證通訊協定。下圖是委派設定的概略內容:

 

接下來我必須將 WCF 應用程式伺服器設成受到信任,以便進行限制委派。這項程序恰巧和我先前所述使用者所要執行的作業一模一樣,因此在 [Active Directory 使用者及電腦] 中已有該電腦帳戶,並已加以設定。以下為其設定內容的圖片連結:

 

 

到此為止,所有與 SharePoint 無關的項目皆已設定完成,可以開始使用了。最後就是準備網頁組件加以測試。

建立 SharePoint 網頁組件

建立網頁組件十分簡單,只要依照前文所述的方式對 SharePoint 進行 WCF 呼叫,並傳遞目前的使用者身分 (https://blogs.technet.com/b/speschka/archive/2010/09/08/calling-a-claims-aware-wcf-service-from-a-sharepoint-2010-claims-site.aspx (可能為英文網頁)) 就可以了。我也可以使用 CASI Kit 進行連接及呼叫 WCF,但我決定手動執行此動作,讓說明可以更加清楚。以下是建立網頁組件的基本步驟:

  1. 在 Visual Studio 2010 中建立新的 SharePoint 2010 專案。
  2. 建立 WCF 服務應用程式的服務參考。
  3. 新增網頁組件。
  4. 將程式碼加入網頁組件,以從 WCF 擷取資料,並將其顯示在資料表中。
  5. 將 Visual Studio 專案中所產生之 app.config 中的所有資訊,加入網頁組件執行所在 Web 應用程式的 web.config 檔案之 <system.ServiceModel> 區段中。

注意: app.config 包含 decompressionEnabled 屬性,在將其加入 WEB.CONFIG 檔案之前,必須先予以刪除。若將其保留在檔案內,網頁組件將會在您嘗試建立服務參考 Proxy 執行個體時傳回錯誤。

上述步驟中除了 #4 之外,每個步驟皆可望文生義,因此將不再多做贅述。以下是網頁組件的程式碼:

private DataGrid dataGrd = null;

private Label statusLbl = null;

 

 

protected override void CreateChildControls()

{

   try

   {

       //create the connection to the WCF and try retrieving the data

       SqlDataSvc.SqlDataClient sqlDC = new SqlDataSvc.SqlDataClient();

 

       //configure the channel so we can call it with FederatedClientCredentials

       SPChannelFactoryOperations.ConfigureCredentials<SqlDataSvc.ISqlData>(

       sqlDC.ChannelFactory, Microsoft.SharePoint.SPServiceAuthenticationMode.Claims);

 

       //create the endpoint to connect to

       EndpointAddress svcEndPt =

          new EndpointAddress("https://az1.vbtoys.com/ClaimsToSqlWCF/SqlData.svc");

 

       //create a channel to the WCF endpoint using the

       //token and claims of the current user

       SqlDataSvc.ISqlData sqlData =

          SPChannelFactoryOperations.CreateChannelActingAsLoggedOnUser

          <SqlDataSvc.ISqlData>(sqlDC.ChannelFactory, svcEndPt);

 

       //request the data

       DataSet ds = sqlData.GetProducts();

 

       if ((ds == null) || (ds.Tables.Count == 0))

       {

          statusLbl = new Label();

          statusLbl.Text = "No data was returned at " + DateTime.Now.ToString();

          statusLbl.ForeColor = System.Drawing.Color.Red;

          this.Controls.Add(statusLbl);

       }

       else

       {

          dataGrd = new DataGrid();

          dataGrd.AutoGenerateColumns = true;

          dataGrd.DataSource = ds.Tables[0];

          dataGrd.DataBind();

          this.Controls.Add(dataGrd);

       }

   }

   catch (Exception ex)

   {

       Debug.WriteLine(ex.Message);

   }

}

 

這組程式碼也十分簡單易懂。第一部分是連線到 WCF 服務,並一併傳送目前使用者的宣告。如需詳細資訊,請參閱前文中所列我先前在部落格上所寫有關這項主題的文章連結。其餘部分就是取回資料集;如有傳回資料,就將資料繫結到資料表,若是失敗,即顯示標籤表示無任何資料。下列三個螢幕擷取畫面說明這些項目如何共同運作:前兩張圖片顯示此程式碼應用在兩位不同使用者上的情況,這兩位使用者會顯示在 CurrentUser 資料欄中。第三張圖片顯示此程式碼應用在一位未獲授權可執行預存程序之使用者上的情況。

 

 

我想應該差不多該結尾了;隨文附上 WCF 服務應用程式及網頁組件的程式碼。由於部落格文章的格式常會出現不同問題,因此一併附上這篇文章的原始 Word 文件。

這是翻譯後的部落格文章。英文原文請參閱 Using SAML Claims, SharePoint, WCF, Claims to Windows Token Service and Constrained Delegation to Access SQL Server