【问题标题】:Wrong Thread.CurrentPrincipal in async WCF end-method异步 WCF 结束方法中的错误 Thread.CurrentPrincipal
【发布时间】:2014-01-27 07:30:04
【问题描述】:

我有一个 WCF 服务,它的 Thread.CurrentPrincipal 设置在 ServiceConfiguration.ClaimsAuthorizationManager 中。

当我像这样异步实现服务时:

    public IAsyncResult BeginMethod1(AsyncCallback callback, object state)
    {
        // Audit log call (uses Thread.CurrentPrincipal)

        var task = Task<int>.Factory.StartNew(this.WorkerFunction, state);

        return task.ContinueWith(res => callback(task));
    }

    public string EndMethod1(IAsyncResult ar)
    {
        // Audit log result (uses Thread.CurrentPrincipal)

        return ar.AsyncState as string;
    }

    private int WorkerFunction(object state)
    {
        // perform work
    }

我发现 Thread.CurrentPrincipal 在 Begin-method 和 WorkerFunction 中设置为正确的 ClaimsPrincipal,但在 End-method 中设置为 GenericPrincipal。

我知道我可以为服务启用 ASP.NET 兼容性并使用 HttpContext.Current.User,它在所有方法中都具有正确的主体,但我不想这样做。

有没有办法在不打开 ASP.NET 兼容性的情况下强制 Thread.CurrentPrincipal 使用正确的 ClaimsPrincipal?

【问题讨论】:

  • 为什么会有这么复杂的代码?这段代码实际上并不是服务调用的异步实现。你得到一个同步调用,将它包装在一个在线程池线程中运行的任务中(它不保留主体),然后将它转换为旧式 APM 方法对 - 为什么?创建一个适当的异步方法而不是静态 WorkerFunction 并将主体作为参数传递,而不是在某些任意线程的属性中设置它
  • @PanagiotisKanavos:代码确实是异步的,因为它是在 APM 方法上运行的 WCF async pattern 的一部分。此外,一旦设置了主体,它在传递给任务或线程池线程时被保留,因为用户主体存储在CallContext 中。
  • @user18044 哎呀,我在考虑 .NET 4.5,您可以在其中定义一个 Task Method1Async 来异步实现 Method1。这对客户端如何生成代理和调用您的方法没有影响,只是对服务代码如何实现合同有影响。您可以轻松拨打client.Method1,电话将转至MethodAsync

标签: wcf async-await wif iprincipal executioncontext


【解决方案1】:

summary of WCF extension points 开始,您会看到专为解决您的问题而设计的那个。它被称为CallContextInitializer。看看这个article which gives CallContextInitializer sample code

如果您进行 ICallContextInitializer 扩展,您将获得对 BeginXXX 线程上下文 EndXXX 线程上下文的控制权。您是说 ClaimsAuthorizationManager 在您的 BeginXXX(...) 方法中正确建立了用户主体。在这种情况下,您然后为自己创建一个自定义 ICallContextInitializer,它分配或记录 CurrentPrincipal,具体取决于它是处理您的 BeginXXX() 还是 EndXXX()。比如:

public object BeforeInvoke(System.ServiceModel.InstanceContext instanceContext, System.ServiceModel.IClientChannel channel, System.ServiceModel.Channels.Message request){
    object principal = null;
    if (request.Properties.TryGetValue("userPrincipal", out principal))
    {
        //If we got here, it means we're about to call the EndXXX(...) method.
        Thread.CurrentPrincipal = (IPrincipal)principal;
    }
    else
    {
        //If we got here, it means we're about to call the BeginXXX(...) method.
        request.Properties["userPrincipal"] = Thread.CurrentPrincipal;            
    }
    ...
 }

为了进一步澄清,请考虑两种情况。假设您同时实现了 ICallContextInitializer 和 IParameterInspector。假设这些挂钩预计将与同步 WCF 服务和异步 WCF 服务一起执行(这是您的特殊情况)。

以下是事件的顺序和对正在发生的事情的解释:

同步案例

ICallContextInitializer.BeforeInvoke();
IParemeterInspector.BeforeCall();
//...service executes...
IParameterInspector.AfterCall();
ICallContextInitializer.AfterInvoke();

上面的代码没什么奇怪的。但是现在看看下面异步服务操作会发生什么......

异步案例

ICallContextInitializer.BeforeInvoke();  //TryGetValue() fails, so this records the UserPrincipal.
IParameterInspector.BeforeCall();
//...Your BeginXXX() routine now executes...
ICallContextInitializer.AfterInvoke();

//...Now your Task async code executes (or finishes executing)...

ICallContextInitializercut.BeforeInvoke();  //TryGetValue succeeds, so this assigns the UserPrincipal.
//...Your EndXXX() routine now executes...
IParameterInspector.AfterCall();
ICallContextInitializer.AfterInvoke();

如您所见,CallContextInitializer 确保您有机会在 EndXXX() 例程运行之前初始化诸如 CurrentPrincipal 之类的值。因此,EndXXX() 例程确实在与 BeginXXX() 例程不同的线程上执行并不重要。是的,System.ServiceModel.Channels.Message 对象在 Begin/End 方法之间存储您的用户主体,即使线程发生更改,WCF 也会保留并正确传输。

总体而言,这种方法允许您的 EndXXX(IAsyncresult) 使用正确的 IPrincipal 执行,而无需在 EndXXX() 例程中显式地重新建立 CurrentPrincipal。与任何 WCF 行为一样,您可以决定这是否适用于单个操作、合同上的所有操作或端点上的所有操作。

【讨论】:

  • 感谢您的详尽回答和指向外部信息的链接!实现 CallContextInitializer 确实解决了这个问题。我的一个问题是,为什么要将主体存储在消息属性中而不是使用 CallContext.SetData(...) 的调用上下文中?如果没有其他答案,我会奖励这个答案。
  • @user18044:CallContext 仅将信息从“父”线程向下传递给从它创建的所有“子”线程。对于 WCF,执行完成回调的线程(来自 BeginXXX() 例程)不是 EndXXX() 线程的父级。相反,完成回调只是通知 WCF 调度程序创建自己的线程来执行 EndXXX() 例程。
【解决方案2】:

并不是我的问题的真正答案,而是实现 WCF 服务(在 .NET 4.5 中)的另一种方法,它不会出现与 Thread.CurrentPrincipal 相同的问题。

    public async Task<string> Method1()
    {
        // Audit log call (uses Thread.CurrentPrincipal)

        try
        {
            return await Task.Factory.StartNew(() => this.WorkerFunction());
        }
        finally 
        {
            // Audit log result (uses Thread.CurrentPrincipal)
        }
    }

    private string WorkerFunction()
    {
        // perform work
        return string.Empty;
    }

【讨论】:

  • +1。实际上,我认为由于 .NET 版本要求,您可能无法公开基于 Task 的 WCF 服务 API。
  • 不,我们使用的是 .NET 4.5,但我们提供了一个 SDK,允许客户在我们的框架中实现他们自己的 WCF 服务。我们提供了一个负责 authN 和 authZ 的服务主机。但是,我无法控制我们的客户如何实现 WCF 服务。我想确保当他们使用 Begin/End 方法时它也能正常工作。
【解决方案3】:

对此的有效方法是创建一个扩展:

public class SLOperationContext : IExtension<OperationContext>
{
    private readonly IDictionary<string, object> items;

    private static ReaderWriterLockSlim _instanceLock = new ReaderWriterLockSlim();

    private SLOperationContext()
    {
        items = new Dictionary<string, object>();
    }

    public IDictionary<string, object> Items
    {
        get { return items; }
    }

    public static SLOperationContext Current
    {
        get
        {
            SLOperationContext context = OperationContext.Current.Extensions.Find<SLOperationContext>();
            if (context == null)
            {
                _instanceLock.EnterWriteLock();
                context = new SLOperationContext();
                OperationContext.Current.Extensions.Add(context);
                _instanceLock.ExitWriteLock();
            }
            return context;
        }
    }

    public void Attach(OperationContext owner) { }
    public void Detach(OperationContext owner) { }
}

现在这个扩展被用作你想要在线程切换之间持久化的对象的容器,因为 OperationContext.Current 将保持不变。

现在您可以在 BeginMethod1 中使用它来保存当前用户:

SLOperationContext.Current.Items["Principal"] = OperationContext.Current.ClaimsPrincipal;

然后在 EndMethod1 中你可以通过输入获取用户:

ClaimsPrincipal principal = SLOperationContext.Current.Items["Principal"];

编辑(另一种方法):

public IAsyncResult BeginMethod1(AsyncCallback callback, object state)
{
    var task = Task.Factory.StartNew(this.WorkerFunction, state);

    var ec = ExecutionContext.Capture();

    return task.ContinueWith(res =>
        ExecutionContext.Run(ec, (_) => callback(task), null));
}

【讨论】:

  • Thread.CurrentPrincipal 的整体理念是它对每个线程都是全局可用的。必须显式保存和恢复变量类型违背了 IMO 的目的。
  • 它是可用的,除非您的延续线程不知道实例化第一个线程的安全上下文(主体)。延续线程不一定与调用 Begin... 的线程相同。
  • 我知道延续线程可能不同。我的问题是如何确保为该线程正确设置了 Thread.CurrentPrincipal。上面 Brent Arias 提出的解决方案以透明的方式做到了这一点。
  • 您是否考虑过放弃 Begin/End+ContinueWith 模式而改用 async/await 模式?因为使用后者,并且将 httpRuntime 设置为 4.5,您可以立即获得所需的内容。但是,要求是您能够在生产环境中使用 .NET 4.5。如果是这种情况,我很乐意为您提供代码示例。
  • 谢谢,感谢您的提议。我知道如何创建基于任务的异步 WCF 服务。这样做可以避免主要问题。但是,我无法控制我们的客户如何创建 WCF 服务。他们可以使用基于任务的实现,或者可以使用 Begin/End APM 方法。在后一种情况下,我只是不知道如何使主体工作。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2013-05-09
  • 1970-01-01
  • 1970-01-01
  • 2018-03-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多