Partager via


使用 SAML 声明、SharePoint、WCF、声明为 Windows 令牌服务和约束委派访问 SQL Server

原文发布于 2011 年 8 月 7 日(星期日)

呵呵,这可能是我写的一篇标题最长的文章了,但我想确保它涵盖所讨论的所有相关技术。最近我听到对此领域的讨论喋喋不休,其实都是关于我如何接受 SAML 声明用户并获取 Windows 上下文来访问其他某个应用程序。SharePoint 2010 为使用“声明为 Windows 令牌服务”(下文简称为 c2wts)提供了有限支持,但仅限于具有少量服务应用程序的 Windows 声明用户。一个常见问题是,它为什么不能使用具有有效 UPN 声明的 SAML 声明用户?确实没有技术上的原因可以解释这一点。因此,考虑到身份验证类型方面的限制以及可使用它的服务应用程序方面的限制,您可能会发现自己处于某种境地,需要通过某种方式将 SAML 用户作为基础 Windows 帐户连接到其他应用程序。希望本文能够帮助您了解执行此操作的基础知识。

此方案要采取的基本方法是创建一个 WCF 服务应用程序,以处理最终用户对其他应用程序(本例中为 SQL Server)数据的所有请求。所以我想采用将点击 SharePoint 网站的 SAML 用户,并在从 SQL Server 检索数据时请求将该 SAML 用户作为 Windows 帐户。注意: 尽管本文讨论的是 SAML 声明用户,但相同的方法也完全适用于 Windows 声明用户;这些用户在登录时会默认获得 UPN 声明。 以下是整个过程的示意图:

配置 SQL Server

我们从 SQL Server 端开始。在我的应用场景中,SQL Server 运行在一台称为“SQL2”的服务器中。SQL 服务本身作为网络服务运行。这意味着我不需要为其创建 SPN;如果它作为域帐户运行,那么我需要为 MSSQLSvc 的该服务帐户创建一个 SPN。在此特定情形中,我将使用旧的罗斯文数据库检索数据。我希望轻松证明发出请求的用户的身份,所以我将“最昂贵的十种产品”(Ten Most Expensive Products) 存储过程修改为:

 

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 工具包发布第 2 部分 (https://blogs.msdn.com/b/sharepoint_chs/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-cn/library/ee517258.aspx。其中包含了您开始操作所需的全部内容。有关 c2wts 的更多背景信息,我还建议您查看 https://msdn.microsoft.com/zh-cn/library/ee517278.aspx

 

注意: 最后这篇文章包含一个重大错误;建议您通过运行以下代码为 c2wts 创建依赖项:sc config c2wts depend=cryptosvc请不要这样做!! 这属于拼写错误,“cryptosvc”不是有效的服务名称,至少在 Windows Server 2008 R2 上如此。如果您这样做,c2wts 将不再启动,因为它会提示依赖项标记为待删除或者找不到。我在此情况下没迷失方向,将依赖项更改为 iisadmin(这是合符逻辑的,因为根据我的情况,至少我的 WCF 主机必须运行,我才能使用 c2wts);否则我就陷入困境了。

配置 Kerberos 约束委派

好了,在有人因为本主题而变得过于兴奋之前,我只想说:

  1. 我不打算深入介绍有关使 kerb 约束委派正常工作的详情。本主题包含的内容太多了。
  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 项就都设置并配置完毕,可以继续操作了。最后需要一个 Web 部件对其进行测试。

创建 SharePoint Web 部件

创建 Web 部件相当简单;我只遵循之前所述的对 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 工具包建立连接并调用 WCF,但我决定手动执行,这样做是为了更便于演示操作过程。创建 Web 部件的基本步骤如下:

  1. 在 Visual Studio 2010 中创建一个新 SharePoint 2010 项目。
  2. 创建一个对我的 WCF 服务应用程序的服务引用。
  3. 添加一个新 Web 部件。
  4. 向 Web 部件中添加代码以从 WCF 检索数据并将其显示在网格中。
  5. 将 Visual Studio 项目中生成的 app.config 中的所有信息添加到将承载 Web 部件的 Web 应用程序的 web.config 文件的 <system.ServiceModel> 节。

注意: app.config 将包含一个名为 decompressionEnabled 的属性;您必须删除该属性才能将其添加到 WEB.CONFIG 文件中。如果保留它,您的 Web 部件将在您尝试创建服务引用代理实例时引发错误。

就上面的步骤而言,除第 4 步之外,其他所有步骤都非常浅显易懂,所以我就不赘述了。下面是 Web 部件的代码:

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 服务应用程序和 Web 部件代码,以及原始 Word 文档(我是在 Word 文档中撰写的这些内容,因为这些文章的格式太老套了)。

这是一篇本地化的博客文章。请访问 Using SAML Claims, SharePoint, WCF, Claims to Windows Token Service and Constrained Delegation to Access SQL Server 以查看原文