使用ASP.NET Web API,我的ExecutionContext不会在异步操作中流动

Dan*_*iel 21 c# asp.net async-await c#-5.0 executioncontext

我很难理解ExecutionContext背后的机制.

从我在线阅读的内容来看,安全性(线程主体),文化等上下文相关的项目应该跨越执行工作单元的边界内的异步线程.

我遇到了非常混乱和潜在危险的错误.我注意到我的线程的CurrentPrincipal在异步执行中丢失了.


以下是ASP.NET Web API方案的示例:

首先,让我们设置一个简单的Web API配置,其中包含两个委托处理程序,用于测试目的.

他们所做的就是写出调试信息并传递请求/响应,除了第一个"DummyHandler",它设置线程的主体以及要在整个上下文中共享的一段数据(请求的相关ID).

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        config.MessageHandlers.Add(new DummyHandler());
        config.MessageHandlers.Add(new AnotherDummyHandler());

        config.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "api/{controller}/{id}",
            defaults: new { id = RouteParameter.Optional }
        );
    }
}

public class DummyHandler : DelegatingHandler
{
    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        CallContext.LogicalSetData("rcid", request.GetCorrelationId());
        Thread.CurrentPrincipal = new ClaimsPrincipal(new ClaimsPrincipal(new ClaimsIdentity(new[]{ new Claim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name", "dgdev") }, "myauthisthebest")));

        Debug.WriteLine("Dummy Handler Thread: {0}", Thread.CurrentThread.ManagedThreadId);
        Debug.WriteLine("User: {0}", (Object)Thread.CurrentPrincipal.Identity.Name);
        Debug.WriteLine("RCID: {0}", CallContext.LogicalGetData("rcid"));

        return base.SendAsync(request, cancellationToken)
                   .ContinueWith(task =>
                       {
                           Debug.WriteLine("Dummy Handler Thread: {0}", Thread.CurrentThread.ManagedThreadId);
                           Debug.WriteLine("User: {0}", (Object)Thread.CurrentPrincipal.Identity.Name);
                           Debug.WriteLine("RCID: {0}", CallContext.LogicalGetData("rcid"));

                           return task.Result;
                       });
    }
}

public class AnotherDummyHandler : MessageProcessingHandler
{
    protected override HttpRequestMessage ProcessRequest(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        Debug.WriteLine("  Another Dummy Handler Thread: {0}", Thread.CurrentThread.ManagedThreadId);
        Debug.WriteLine("  User: {0}", (Object)Thread.CurrentPrincipal.Identity.Name);
        Debug.WriteLine("  RCID: {0}", CallContext.LogicalGetData("rcid"));

        return request;
    }

    protected override HttpResponseMessage ProcessResponse(HttpResponseMessage response, CancellationToken cancellationToken)
    {
        Debug.WriteLine("  Another Dummy Handler Thread: {0}", Thread.CurrentThread.ManagedThreadId);
        Debug.WriteLine("  User: {0}", (Object)Thread.CurrentPrincipal.Identity.Name);
        Debug.WriteLine("  RCID: {0}", CallContext.LogicalGetData("rcid"));

        return response;
    }
}
Run Code Online (Sandbox Code Playgroud)

很简单.接下来让我们添加一个ApiController来处理HTTP POST,就像上传文件一样.

public class UploadController : ApiController
{
    public async Task<HttpResponseMessage> PostFile()
    {
        Debug.WriteLine("    Thread: {0}", Thread.CurrentThread.ManagedThreadId);
        Debug.WriteLine("    User: {0}", (Object)Thread.CurrentPrincipal.Identity.Name);
        Debug.WriteLine("    RCID: {0}", CallContext.LogicalGetData("rcid"));

        if (!Request.Content.IsMimeMultipartContent())
        {
            throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType);
        }

        try
        {
            await Request.Content.ReadAsMultipartAsync(
                new MultipartFormDataStreamProvider(
                    HttpRuntime.AppDomainAppPath + @"upload\temp"));

            Debug.WriteLine("    Thread: {0}", Thread.CurrentThread.ManagedThreadId);
            Debug.WriteLine("    User: {0}", (Object)Thread.CurrentPrincipal.Identity.Name);
            Debug.WriteLine("    RCID: {0}", CallContext.LogicalGetData("rcid"));

            return new HttpResponseMessage(HttpStatusCode.Created);
        }
        catch (Exception e)
        {
            return Request.CreateErrorResponse(HttpStatusCode.InternalServerError, e);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

在使用Fiddler运行测试时,这是我收到的输出:

Dummy Handler Thread: 63
User: dgdev
RCID: 6d542847-4ceb-4511-85e5-d1b5bf3be476

  Another Dummy Handler Thread: 63
  User: dgdev
  RCID: 6d542847-4ceb-4511-85e5-d1b5bf3be476

    Thread: 63
    User: dgdev
    RCID: 6d542847-4ceb-4511-85e5-d1b5bf3be476

    Thread: 77
    User:                                     <<<  PRINCIPAL IS LOST AFTER ASYNC
    RCID: 6d542847-4ceb-4511-85e5-d1b5bf3be476

  Another Dummy Handler Thread: 63
  User:                                       <<<  PRINCIPAL IS STILL LOST
  RCID: 6d542847-4ceb-4511-85e5-d1b5bf3be476

Dummy Handler Thread: 65
User: dgdev                                   <<<  PRINCIPAL IS BACK?!?
RCID: 6d542847-4ceb-4511-85e5-d1b5bf3be476
Run Code Online (Sandbox Code Playgroud)

更令人困惑的是,当我将跟随追加到异步行时:

await Request.Content.ReadAsMultipartAsync(
    new MultipartFormDataStreamProvider(..same as before..))
.ConfigureAwait(false); <<<<<<
Run Code Online (Sandbox Code Playgroud)

我现在收到这个输出:

Dummy Handler Thread: 40
User: dgdev
RCID: 8d944500-cb52-4362-8537-dab405fa12a2

  Another Dummy Handler Thread: 40
  User: dgdev
  RCID: 8d944500-cb52-4362-8537-dab405fa12a2

    Thread: 40
    User: dgdev
    RCID: 8d944500-cb52-4362-8537-dab405fa12a2

    Thread: 65
    User: dgdev                               <<<  PRINCIPAL IS HERE!
    RCID: 8d944500-cb52-4362-8537-dab405fa12a2

  Another Dummy Handler Thread: 65
  User:                                       <<<  PRINCIPAL IS LOST
  RCID: 8d944500-cb52-4362-8537-dab405fa12a2

Dummy Handler Thread: 40
User: dgdev
RCID: 8d944500-cb52-4362-8537-dab405fa12a2
Run Code Online (Sandbox Code Playgroud)

这里的重点是这一点.事实上,async后面的代码调用了我的业务逻辑,或者只需要正确设置安全上下文.存在潜在的完整性问题.

任何人都可以帮助解决一些正在发生的事情吗?

提前致谢.

Ste*_*ary 26

我没有所有的答案,但我可以帮助填补一些空白并猜测问题.

默认情况下,ASP.NET SynchronizationContext将流动,但它流动身份的方式有点奇怪.它实际上流动HttpContext.Current.User然后设置Thread.CurrentPrincipal为.因此,如果您只是设置Thread.CurrentPrincipal,您将看不到它正确流动.

实际上,您将看到以下行为:

  • 从在Thread.CurrentPrincipal线程上设置的时间开始,该线程将具有相同的主体,直到它重新进入ASP.NET上下文.
  • 当任何线程进入ASP.NET上下文时,将Thread.CurrentPrincipal被清除(因为它被设置为HttpContext.Current.User).
  • 当一个线程在ASP.NET上下文之外使用时,它只保留Thread.CurrentPrincipal发生在它上面的任何事情.

将其应用于原始代码和输出:

  • 前三个都是CurrentPrincipal在明确设置后从线程63同步报告的,因此它们都具有预期值.
  • 线程77用于恢复该async方法,从而进入ASP.NET上下文并清除CurrentPrincipal它可能具有的任何内容.
  • 线程63用于ProcessResponse.它重新进入ASP.NET上下文,清除它Thread.CurrentPrincipal.
  • 线程65是有趣的.它在ASP.NET上下文之外运行(在ContinueWith没有调度程序的情况下),因此它只保留CurrentPrincipal它之前发生的任何事情.我假设它CurrentPrincipal刚刚从早期的测试运行中遗留下来.

更新后的代码将更PostFile改为在ASP.NET上下文之外运行其第二部分.所以它选择了刚刚CurrentPrincipal设置的线程65 .因为它在ASP.NET上下文之外,CurrentPrincipal所以不会被清除.

所以,它看起来像ExecutionContext流淌的好.我确信微软已经测试ExecutionContext过流出了wazoo; 否则,世界上每个ASP.NET应用程序都会有严重的安全漏洞.重要的是要注意,在此代码中Thread.CurrentPrincipal仅引用当前用户的声明,并不代表实际模仿.

如果我的猜测是正确的,那么修复很简单:在SendAsync,更改此行:

Thread.CurrentPrincipal = new ClaimsPrincipal(new ClaimsPrincipal(new ClaimsIdentity(new[]{ new Claim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name", "dgdev") }, "myauthisthebest")));
Run Code Online (Sandbox Code Playgroud)

对此:

HttpContext.Current.User = new ClaimsPrincipal(new ClaimsPrincipal(new ClaimsIdentity(new[]{ new Claim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name", "dgdev") }, "myauthisthebest")));
Thread.CurrentPrincipal = HttpContext.Current.User;
Run Code Online (Sandbox Code Playgroud)

  • 斯蒂芬,我不能完全感谢你的清晰答案.我读过你之前关于ExecutionContext的问题,以及你和Stephen Toub关于这个主题的博客.仍然无法确定我的问题的原因.再次感谢!你的修复工作完美. (2认同)