Windows 工作流

保护 WF 4 工作流服务的安全

Zulfiqar Ahmed

下载代码示例

Windows Workflow Foundation (WF) 为编写软件逻辑提供了可视化的创作体验。一旦软件逻辑实现为工作流,您只需将该工作流部署到工作流宿主上就可以执行了。工作流服务是一种通过工作流实现的特殊 Web 服务。该服务在部署到 WorkflowServiceHost 上之后即可提供给用户使用。本文将介绍各种工作流宿主的安全选项,尤其是工作流服务和 WorkflowServiceHost 的安全选项。我还介绍一些可以将安全范围从工作流服务扩展到工作流层的重要扩展功能。另外,还将讨论工作流安全包 (WFSP) 项目,及其活动集在工作流解决方案中实现端到端安全性的方法。作为 Microsoft .NET Framework 4 组件推出的 WF 4 提供了一个可扩展的宿主 API,并自带三种现成的具有不同功能的宿主。

WorkflowInvoker。这是最简单也是功能最少的宿主接口,提供了简单的工作流调用 API。一个 WorkflowInvoker 对象只支持一个工作流实例,并要通过构造函数或静态 Invoke 方法传入该实例。由于所有工作流定然通过同一调用线程来执行,因此,如果调用代码在模拟特定的安全上下文,那么所有活动就都在该模拟上下文中执行。WorkflowInvoker 并不是真正意义上的工作流宿主;因为,它不但封装基于 WorkflowApplication 的宿主,并使用基于泵的同步环境来提供包含统一执行语义的易用型 API。例如,异常和事务等活动可以跨调用边界无缝流动。这种行为简化了安全性,因为调用方的安全上下文在工作流执行期间一直处于可用状态,并且活动可以在各种安全方案下使用这个上下文。例如,主体权限授权和 Kerberos 委派活动可与 WorkflowInvoker 无缝协作。

WorkflowApplication。这是一个功能略强一点的宿主,不过它仍只支持一个实例。该宿主通过 CLR ThreadPool 中的 IO 线程来执行工作流。调用线程的安全上下文不会复制到相应的工作流线程,因此,即使工作流客户端在执行模拟操作,执行活动的 WF 线程也不会执行模拟操作。通过使用自定义同步环境在同一传入异步线程上转发调用(类似于 WorkflowInvoker 所用的同步环境),可将调用方的安全上下文传给 WF 线程:

public class MySyncContext : SynchronizationContext
{
  public override void Post(SendOrPostCallback d, object state)
  {
    d(state);
  }
}

WorkflowServiceHost。这是最全面的宿主,提供的宿主环境可以调用多个工作流实例。工作流服务是一种可以通过工作流实现的特殊 Web 服务。WorkflowServiceHost 派生自标准 Windows Communication Foundation (WCF) ServiceHostBase 类,支持所有的 WCF 安全概念。消息传送活动是 WorkflowServiceHost 和 WorkflowHostingEndpoint(支持在不必使用消息传送活动的情况下使用 WorkflowServiceHost)支持的主要交互模型。本文重点介绍消息传送活动、工作流服务和 WorkflowServiceHost 的安全主题。要概括了解工作流服务技术,请参阅 Leon Welicki 在 2010 年 5 月期 MSDN 杂志 上的文章“使用 WCF 和 WF 4 的工作流可视化设计”(msdn.microsoft.com/magazine/ff646977)。

工作流服务网络安全

由于工作流服务是标准的 WCF 服务,因此,网络安全功能可以通过标准 WCF 绑定机制来配置。工作流服务可以根据其安全需要,通过一个或多个使用特定绑定的端点来公开。WCF 分派管道只有在传入消息符合目标端点的安全要求时才会执行。工作流逻辑在分派管道的末端执行,因此,所有常用的 WCF 扩展功能同样适用于工作流服务。例如,也可以使用标准 ServiceAuthorizationManager 扩展功能来应用工作流服务的授权功能。Windows Identity Foundation (WIF) 等框架可以在调度程序级别与 WCF 集成,并且它们还可以透明地用于工作流服务。线程处理方面存在一些差异,这些差异与 WCF 和 WF 层之间的几个异步点有关。由于这些差异的存在,某些与线程本地存储 (TLS) 相关的方案遇到的工作流服务问题会多一些。例如,WIF 通过 Thread.CurrentPrincipal 公开传入的身份信息,并可以为基于代码的服务正确设置此属性。然而,工作流逻辑最终可能不会在原始 WCF 线程上执行。这样,所有与 TLS 相关的数据(包括 Thread.CurrentPrincipal)都会变成无效数据,因此,建议不要在工作流中依赖 TLS。后面,我会介绍一些解决此问题的可能方法。

WF 4 还提供一个 Send 活动,用以从某个工作流中调用其他 Web 服务。可以为 Send 活动配置一个绑定,以在从工作流中调用其他服务时使用。在内部,Send 活动使用标准 WCF ChannelFactory/Channel API 发送消息,而配置的绑定则可以用来创建此内部通道工厂。Send 活动还内置一个缓存层,用来缓存 ChannelFactory/Channel。默认情况下,只有在通过 Send 活动的属性直接指定了端点信息,并且选择了现有绑定后,才能使用该缓存层,如图 1 所示。

图 1 Send 活动属性

通过 EndpointConfigurationName 属性从配置文件加载端点信息后,相应的安全缓存功能就会进入禁用状态,并且您每次执行 Send 活动后,系统都会创建一个全新的 ChannelFactory。在通道工厂启动阶段,保护 wsHttpBinding、wsFederationHttpBinding 等绑定是很麻烦的事,而且为每条消息重建通道工厂的成本也很大。例如,默认的 WSHttpBinding 已经经过优化,可以提高性能和安全性。虽然这样需要耗费前期成本来创建安全的会话,但却可以确保后续消息的安全,大幅降低相应的成本。如果没有 ChannelFactory 缓存功能,WSHttpBinding 的这一优势就会变成劣势,因为系统每发送一条业务消息就会多发四条基础结构消息来协商服务凭据,然后创建安全的会话。如图 2 所示。

图 2 为协商服务凭据而发出的基础结构消息

https://schemas.xmlsoap.org/ws/2005/02/trust/RST/Issue
https://schemas.xmlsoap.org/ws/2005/02/trust/RSTR/Issue
https://schemas.xmlsoap.org/ws/2005/02/trust/RST/SCT
http://tempuri.org/IPingService/Ping
https://schemas.xmlsoap.org/ws/2005/02/trust/RST/SCT/Cancel

在 WF 4 中,对于任何从配置文件中加载的绑定配置(甚至是默认绑定),Send 活动都会将其视为“不安全的缓存方法”,并且系统会禁用 ChannelFactory 缓存功能。强制应用不安全的通道缓存功能可以覆盖默认行为,让您能够重新使用 ChannelFactory 向同一端点发送消息。SendMessageChannelCache 服务行为不但可以启用不安全的缓存功能,而且还可以配置各种 Channel 和 ChannelFactory 缓存设置,如下所示:

<serviceBehaviors>
  <behavior>
    <sendMessageChannelCache allowUnsafeCaching="true">
      <channelSettings idleTimeout="1:0:0" maxItemsInCache="60"/>
      <factorySettings idleTimeout="1:0:0" maxItemsInCache="60"/>
    </sendMessageChannelCache>
  </behavior>
</serviceBehaviors>

OperationContext 在哪里?

在基于代码的传统服务中,可以通过 OperationContext.Current 提供传入调用的安全信息。 由于 WCF 调度程序运行时会在您调用该服务方法前在相应线程上设置 OperationContext,因此,您在该服务方法中可以通过 OperationContext.Current 来访问安全信息。

工作流服务会增加复杂性,因为,在 WCF 调度程序和工作流执行之间存在大量异步点。 这些异步点中的任何一个都可以切换线程,如果发生线程切换,那么工作流逻辑(活动)就会在非 WCF 线程上执行,并且将无法通过 OperationContext.Current 方法来访问 OperationContext。 在 WF 4 中,通过基于 IReceiveMessageCallback 和 ISendMessageCallback 的回调机制,可以不受线程影响访问 OperationContext。 IReceiveMessageCallback 和 ISendMessageCallback 分别让您可以在服务器端和客户端访问 OperationContext。 在服务器端,在 Receive 活动收到消息后立即调用 IReceiveMessageCallback。 要将这些回调添加到 Send 和 Receive 活动中,必须确保它们可以在 Send/Receive 执行时用作执行属性。 将执行属性添加到活动的常用方法是创建父级范围活动,然后将执行属性添加到父级活动的执行环境中,如图 3 所示。

图 3 要添加执行属性的简单范围活动

[ContentProperty("Body")]
public sealed class OperationContextScope : NativeActivity
{
  public Activity Body { get; set; }
  protected override void Execute(NativeActivityContext context)
  {
    if (this.Body != null)
    {
      // Adding an execution property to handle OperationContext
      context.Properties.Add(OperationContextScopeProperty.Name,
        new OperationContextScopeProperty());
      context.ScheduleActivity(this.Body);
    }
  }
}

图 3 的代码段中,OperationContextScope 活动在执行时只是将执行属性添加到相应的环境中,以使所有子级活动都可以访问该执行属性。 Send 和 Receive 活动会查找上述回调属性,如果发现其中任何一个属性,就会在消息处理的适当阶段调用相应的属性,这样,您就可以访问 OperationContext 了。 在此示例中,任何将属同一范围的 Receive 活动都可以访问 OperationContextScopeProperty,并可以执行回调,传入 OperationContext 值(参见图 4)。

图 4 实现为执行属性的 ReceiveMessageCallback

[DataContract]
class OperationContextScopeProperty : IReceiveMessageCallback,  IExecutionProperty
{
  private OperationContext current;
  private OperationContext orignal;

  public static readonly string Name = 
    typeof(OperationContextScopeProperty).FullName;
  public void OnReceiveMessage(OperationContext operationContext, 
    ExecutionProperties activityExecutionProperties)
  {
    current = operationContext;
    operationContext.OperationCompleted 
      += delegate(object sender, EventArgs e)
    {
      current = null;
    };
  }

  public void CleanupWorkflowThread()
  {
    OperationContext.Current = orignal;
  }
  public void SetupWorkflowThread()
  {
    orignal = OperationContext.Current;
    OperationContext.Current = current;
    }
}

OperationContextScopeProperty 只是捕获和存储当前活动的 OperationContext,然后通过 WF TLS 机制将其设置到适当的 WF 线程上。IExecutionProperty 接口提供了 Setup/CleanUpWorkflowThread 方法,这些方法可以在每个 WF 工作项(活动)执行前后接受调用,而且还可以在选定的 WF 线程上设置各种与 TLS 相关的属性(如本例中的 OperationContext)。

OperationContextScope 是一个自定义的活动,它通过 WF 4 扩展功能可以在不受线程影响的情况下访问所有范围内子级活动的 WCF OperationContext。

工作流服务和 WIF

WIF 提供了大量用来声明启用 WCF 服务和 ASP.NET 应用程序的 API 和对象模型。WIF 在宿主级别集成 WCF,因此,大部分 WIF 功能也能支持工作流服务。有关如何集成 WIF 和工作流服务的详细信息,请参见我的博文 bit.ly/a6pWgA。这一现成的集成功能适合用来支持基本方案,而对于其他高级方案,可以通过 WFSP 中的活动来实现。

WFSP CTP 1 简介

WFSP Community Technology Preview (CTP) 1 提供了各种用来帮您在 WF 4 中实现主要安全方案的活动和相关 WCF 行为。WFSP 利用 ISend/IReceiveMessageCallback 扩展模型来实现它的很多功能。WFSP CTP 1 已于 2010 年 7 月在 CodePlex 上发布,可以从 wf.codeplex.com/releases/view/48114 下载。

WFSP 活动(如图 5 所示)可以有效地与 WF 的其他功能集成,并且还提供能在工作流解决方案中应用集成安全方案的强大结构。

图 5 工作流安全包活动

工作流中的授权

在工作流服务中,您可以使用标准 WCF ServiceAuthorizationManager 扩展功能来实现授权,并且该功能的运行方式与基于代码的服务完全相同。但是,在某些情况下(如授权数据位于工作流中时),您可能要到工作流实际执行前才做出授权决策。PrincipalPermissionScope 是一个可以用来在工作流中添加 CLR PrincipalPermission 授权功能的服务器端活动。位于该范围之内的任何子级活动只有满足了相应的权限要求后才能执行。该活动负责查找在 OperationContext 中传入 WCF 安全上下文的身份信息。PrincipalPermissionScope 通过本文上述的同一 IReceiveMessageCallback 机制来实现。

实际的 PrincipalPermission 要求是根据 ServiceAuthorizationBehavior.PrincipalPermissionMode 的值来约束 IPrincipal 对象的。该扩展功能可以让 PrincipalPermissionScope 支持 ASP.NET 角色提供程序以及自定义 IAuthorizationPolicy 所生成的自定义 IPrincipal 实现。有关如何配置用于 WCF 服务的 ASP.NET 角色提供程序的方法,请参见 msdn.microsoft.com/library/aa702542

消息传送活动和经验证的消息传送

Send 活动提供了在工作流中使用 Web 服务的主要方法。在大多数实际应用中,这些后端服务都是受保护的,它们在执行任何操作之前都会要求进行身份验证。在基于代码的标准服务中,凭据信息通过 ChannelFactory 和 ClientBase<T> 派生代理类公开的 ClientCredential 属性来指定。使用该属性,客户端可以指定其在调用服务前要使用的凭据。遗憾的是,包装 ChannelFactory 的 Send 活动在 WF 4 中未公开 ClientCredential,因此,通过现成的 Send 活动无法实现某些需要显式指定凭据的方案。注意,Send 活动却可以从配置文件中选取端点行为配置,因此,您可以通过创建自定义端点行为来指定这些凭据。

在要求显式提供凭据的操作中,调用要求提供用户名/密码的服务就是常见的一种。由于 Send 活动不公开 ClientCredential 属性,因此,您无法指定用户名/密码。下面,我们来了解一下 WFSP 中的 GetUserNameSecurityToken 活动是如何解决这类问题及其他相关问题的。

图 6 中,“添加服务引用”向导生成 Ping 活动,该活动配置为调用一个要求进行 UserName 身份验证的受保护服务,如下面的绑定配置所示:

<wsHttpBinding>
  <binding name="singleShotUserName">
    <security mode="Message">
      <message clientCredentialType="UserName" establishSecurityContext
        ="false" />
    </security>
  </binding>
</wsHttpBinding>

图 6 使用 UserName 令牌进行经验证的消息传送

在上述工作流中,GetUserNameSecurityToken 首先会根据提供的 UserName/Password 来创建一个 UserNameSecurityToken,然后将该令牌登记到 TokenFlowScope 活动提供的环境 SecurityTokenHandle 之中。“工作流安全包”与 ChannelFactory/ClientBase<T> 方法应用安全功能的方式不同,前者在 SecurityToken 级别应用,而后者在凭据级别应用。在标准 WCF 中,凭据是用来创建安全令牌的,但 WFSP 会直接在令牌级别(而非凭据级别)将安全令牌应用到 WCF 安全层。

TokenFlowScope 是确保消息传送操作和其他相关操作通过验证的重要活动。该活动和 WorkflowClientCredentials 端点行为都可以将登记的令牌从工作流层应用到 WCF 安全层,这样,您就能根据端点的绑定要求为传出消息附加这些令牌了。TokenFlowScope 要求您配置自定义的 ClientCredential 行为 (WorkflowClientCredentials),配置代码如下所示:

<behavior>
   <!--This custom clientCredentials enables the credential flow from 
     workflow data model into WCF security layer.
-->
   <clientCredentials
     type="Microsoft.Security.Activities.WorkflowClientCredentials, 
     Microsoft.Security.Activities, Version=1.0.0.0, Culture=neutral, 
     PublicKeyToken=31bf3856ad364e35">
   </clientCredentials>
</behavior>

WFSP 在调用要求提供安全令牌服务 (STS) 令牌的服务时完全依照此模型操作,如图 7 所示。

图 7 精细控制 SAML 令牌的获取和使用

图 7 中,GetSamlSecurityToken 先联系颁发者获取一个 SAML 令牌,然后将该令牌登记到 TokenFlowScope 活动所提供的环境句柄。通过登记,可以让所有位于同一范围并要求提供 SAML 令牌的 Send 活动访问该令牌。这个模型是可扩展的,并且 GetSamlSecurityToken 自身也可以在获取 SAML 令牌时使用已登记的令牌(例如,如果 STS 要求提供 UserName 令牌才能返回 SAML 令牌,而同一范围内已存在有效的 UserName 令牌)。GetSamlSecurityToken 在配置了 WorkflowClientCredentials 行为的情况下也会在请求 SAML 令牌时使用此令牌。

现成的 WFSP 只支持 UserName 和 SAML 令牌类型;不过,您可以通过从 GetSecurityToken 类继承的方式实现其他令牌类型。如图 8 中的代码段所示,该代码段通过实现一个活动创建基于 X509 的令牌。

图 8 WFSP 扩展功能:实现其他令牌类型

[Designer(typeof(GetX509SecurityTokenDesigner))]
public class GetX509SecurityToken : GetSecurityToken
{
  public GetX509SecurityToken()
  {
    FindType = X509FindType.FindBySubjectName;
    StoreLocation = StoreLocation.CurrentUser;
    StoreName = StoreName.My;
  }

  public InArgument<X509Certificate2> Certificate { get; set; }
  public X509FindType FindType { get; set; }
  public StoreLocation StoreLocation { get; set; }
  public InArgument<string> FindValue { get; set; }
  public StoreName StoreName { get; set; }

  protected override void Execute(NativeActivityContext context)
  {
    X509Certificate2 targetCert = null;
    if (this.Certificate != null)
      targetCert = this.Certificate.Get(context);
    if (targetCert == null)
    {
      var store = new X509Store(StoreName, StoreLocation);
      try
      {
        store.Open(OpenFlags.ReadOnly);
        var col = store.Certificates.Find(FindType, FindValue.Get(context), false);
        if (col.Count > 0)
          targetCert = col[0];//Use first certificate mathing the search criteria
      }
      finally
      {
        if (store != null)
          store.Close();
      }
    }
    if (targetCert == null)
      throw new InvalidOperationException(
        "No certificate found using the specified find criteria.");
        // Enlist the token as a flow token
      base.EnlistSecurityToken(context, new X509SecurityToken(targetCert));
  }
}

GetX509SecurityToken 首先根据证书创建一个 X509Security 令牌,然后将其登记到 SecurityTokenHandle 作为一个流令牌,该令牌随后可用于调用要求提供验证证书的服务。图 9 显示的是用于自定义活动设计器的 GetX509SecurityToken。

图 9 包含自定义 GetToken 活动的 TokenFlowScope

基于声明的委派

基于声明的委派是通过 WFSP 实现的另外一种有用功能。通常,基于声明的委派功能在工作流服务中具有更重要的地位,因为这些服务主要用来实现调用多个后端服务的业务流程/业务过程。另外,访问调用方在这些后端服务中的身份信息通常需要做出精细的授权决定。WFSP 可以通过 WS-Trust 1.4 的 ActAs 功能将任何类型的令牌转换成 ActAs 令牌。默认情况下,所有 GetToken 活动都可以创建令牌并将其登记为流令牌。但这些活动都有一个 IsActAsToken 标记,如图 10 所示。

图 10 创建 ActAs 令牌

选中该标记后,令牌创建逻辑没有变化,但创建的令牌 T1 会登记为 ActAs 令牌,而不是流令牌。每个范围只能有一个 ActAs 令牌,供 GetSamlSecurityToken 活动在请求 SAML 令牌时使用。GetSamlSecurityToken 执行时,系统会选择活动的 ActAs 令牌,并将其作为 GetSamlSecurityToken 活动所生成的令牌颁发请求中的内容发送。返回的令牌 T2 会包含身份验证令牌和 ActAs 令牌的声明。最后,对于任何在此范围内执行的 Send 活动,在调用可在其安全上下文中同时看到这两个身份信息的后端服务时,都可以使用此 T2 令牌。

GetBootstrapToken 活动用在中间层方案中,用以实现端到端的基于声明的委派。与 GetToken 活动不同,该活动只负责读取传入的令牌,然后将其登记为 ActAs 令牌,而不创建和登记新令牌。在调用后端服务时,GetBootstrapToken 活动让工作流服务除了使用自身的身份以外,还可以使用传入调用方的身份,如图 11 所示。

图 11 基于声明的端到端委派流

图 11 的步骤 3 中,工作流服务通过 WFSP 活动读取传入的引导令牌,获取充当引导令牌身份的新令牌,并将两个身份都传给后端服务器。图 12 显示的是此工作流服务使用的工作流。

图 12 基于声明的委派工作流

在图 12 所示的工作流中,GetBootstrap 活动放在了 OperationContextScope 内部,这是为了确保该活动在执行时可以不受线程的影响访问 OperationContext。GetSamlSecurityToken 使用 GetBootstrapToken 活动在上一步骤生成的 ActAs 令牌。最后,Echo 活动通过 GetSamlSecurityToken 活动生成的最终 SAML 令牌调用后端服务。

Windows 模拟/委派

ImpersonatingReceiveScope 是又一个可以将 Windows 模拟和委派功能添加到工作流环境中的服务器端活动。该活动在执行时查找传入安全上下文中的 WindowsIdentity。如果传入消息生成了 WindowsIdentity,那么,该活动的所有子级活动都在这一模拟范围内部执行。ImpersonatingReceiveScope 会在执行工作项之前,通过本文前面所述 Workflow TLS 机制来模拟 WF 线程身份。当 WF 工作项完成执行后,模拟身份恢复成原有身份。

如果 ImpersonatingReceiveScope 在传入安全上下文中未找到有效的 WindowsIdentity,它就会查找 WIF 身份 (Thread.CurrentPrincipal) 或传统 WCF ClaimsSet 中的 UPN 声明,然后通过 Kerberos 的 S4U 功能使用该声明来创建 WindowsToken。要将 UPN 声明转换成 Windows 令牌,ImpersonatingReceiveScope 需要使用 WIF 运行时中的“声明到 Windows 令牌转换服务”。要确保声明到令牌的转换成功,必须安装并运行该服务。

图 13 显示的是 ImpersonatingReceiveScope 活动的典型用法。

图 13 运行中的 ImpersonatingRecieveScope

端到端安全性

从外部看来,工作流服务就是标准的 WCF 服务,因此大部分 WCF 安全功能同样适用于工作流服务。WF 4 引入了几项可以将安全范围从工作流服务扩展到工作流层的重要扩展功能。WFSP 提供了一系列可以使用这些扩展功能为 WF 4 实现端到端安全性的活动。

Zulfiqar Ahmed 是高级开发支持团队的高级顾问,也是工作流安全包项目的原创者。他经常在 zamd.net 上发表博文,内容涵盖 Windows Communication Foundation、Windows Workflow Foundation、基于声明的安全技术和常规 Microsoft .NET Framework 等方面的主题。

衷心感谢以下技术专家对本文的审阅:Dave Cliffe