在异步WCF终结方法中错误的Thread.CurrentPrincipal

9

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

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

    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
    }

我发现在Begin方法和WorkerFunction中Thread.CurrentPrincipal被设置为正确的ClaimsPrincipal,但在End方法中它被设置为GenericPrincipal。

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

是否有一种方法可以强制Thread.CurrentPrincipal设置为正确的ClaimsPrincipal而不启用ASP.NET兼容性?


为什么要这样复杂的代码?这段代码并不是一个异步实现的服务调用。你得到了一个同步调用,将其包装在一个在线程池线程中运行的任务中(该任务不保留主体),然后将其转换为旧式的APM方法对 - 为什么?创建一个适当的异步方法,而不是静态WorkerFunction,并将主体作为参数传递,而不是在某个任意线程的属性中进行设置。 - Panagiotis Kanavos
@PanagiotisKanavos:代码确实是异步的,因为它是WCF异步模式的一部分,该模式使用APM方法。此外,一旦设置了主体,当传递给任务或线程池线程时,它被保留,因为用户主体存储在CallContext中。 - Brent Arias
@user18044 抱歉,我想的是 .NET 4.5,在那里你可以定义一个 Task<string> Method1Async 来异步实现 Method1。这不会影响客户端生成代理和调用方法,仅仅只是影响服务代码如何实现合同。你可以轻松地调用 client.Method1,调用将被路由到 MethodAsync - Panagiotis Kanavos
3个回答

4
WCF扩展点概述开始,您将看到一个专门设计来解决您问题的扩展点。它被称为CallContextInitializer。请查看这篇文章,其中提供了CallContextInitializer示例代码
如果您创建一个ICallContextInitializer扩展,您将控制BeginXXX线程上下文和EndXXX线程上下文。您说ClaimsAuthorizationManager已经在您的BeginXXX(...)方法中正确地建立了用户主体。在这种情况下,您可以创建一个自定义的ICallContextInitializer,根据处理BeginXXX()还是EndXXX(),分配或记录CurrentPrincipal。例如:
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()例程不同的线程上执行,也没有关系。是的,存储您的用户主体在Begin/End方法之间的System.ServiceModel.Channels.Message对象会被WCF保留并正确传输,即使线程发生了变化。
总的来说,这种方法允许EndXXX(IAsyncresult)使用正确的IPrincipal执行,而无需在EndXXX()例程中显式重新建立CurrentPrincipal。与任何WCF行为一样,您可以决定是否适用于单个操作、合同上的所有操作或端点上的所有操作。

感谢您提供详细的回答和外部信息链接!实现CallContextInitializer确实解决了问题。我的一个问题是,为什么要将主体存储在消息属性中,而不是使用CallContext.SetData(...)将其存储在调用上下文中?如果没有其他答案,我将授予此答案赏金。 - MvdD
@user18044:CallContext仅将信息从“父”线程向下传递到由其创建的所有“子”线程。在WCF的情况下,执行完成回调的线程(来自BeginXXX()例程)不是EndXXX()线程的父线程。相反,完成回调仅仅是向WCF分派程序发出信号,以创建自己的线程来执行EndXXX()例程。 - Brent Arias

1

这并不是我问题的答案,而是一种实现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-based WCF服务API。 - noseratio - open to work
1
不,我们使用的是.NET 4.5,但我们提供了一个SDK,允许客户在我们的框架中实现自己的WCF服务。我们提供一个服务主机来处理身份验证和授权。然而,我无法控制我们的客户如何实现WCF服务。我想确保当他们使用Begin/End方法时,它也能正常工作。 - MvdD

0

在这个问题上,一个有效的方法是创建一个扩展:

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 的整个概念是它对于每个线程都是全局可用的。我认为,必须明确保存和恢复变量有点违背了它的目的。 - MvdD
你是否考虑放弃Begin/End+ContinueWith模式,改用async/await模式呢?因为使用后者,并且将httpRuntime设置为4.5,你可以直接得到想要的结果。然而,前提是你的生产环境能够使用.NET 4.5。如果是这种情况,我很乐意为你提供代码示例。 - Admir Tuzović
谢谢,感谢提供帮助。我知道如何创建基于任务的异步WCF服务,并且这样做可以避免主要问题。然而,我无法控制我们的客户如何创建WCF服务。他们可能使用基于任务的实现,也可能使用Begin/End APM方法。我只是无法弄清楚如何在后一种情况下使主要工作正常运行。 - MvdD
好的,解释得很清楚。我已经发布了另一种方法,你介意测试一下吗?它将您当前的ExecutionContext放入SynchronizationContext中,也应该解决这个问题。核心功能是执行ExecutionContext.Capture()并在创建继续任务时将其作为参数传递。 - Admir Tuzović
我已经测试了你提供的另一种方法。我的问题最初由另一个发布了这个解决方案的人回答。不幸的是,在我在评论中告诉他捕获的ExecutionContext的SecurityContext属性为空时,他删除了他的答案。至少这给了我重新添加丢失信息的机会。 - MvdD
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接