代码之家  ›  专栏  ›  技术社区  ›  Daniel

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

  •  22
  • Daniel  · 技术社区  · 11 年前

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

    根据我在网上读到的内容,上下文敏感的项目,如安全性(线程主体)、区域性等,应该在执行工作单元的范围内跨异步线程流动。

    不过,我遇到了非常令人困惑和潜在危险的bug。我注意到我的线程的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;
        }
    }
    

    很简单。接下来,让我们添加一个ApiController来处理HTTPPOST,就好像您在上传文件一样。

    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);
            }
        }
    }
    

    在使用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
    

    更令人困惑的是,当我将以下内容附加到异步行时:

    await Request.Content.ReadAsMultipartAsync(
        new MultipartFormDataStreamProvider(..same as before..))
    .ConfigureAwait(false); <<<<<<
    

    我现在收到这个输出:

    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
    

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

    有人能帮助我们了解一下正在发生的事情吗?

    提前谢谢。

    2 回复  |  直到 11 年前
        1
  •  29
  •   Stephen Cleary    11 年前

    我不知道所有的答案,但我可以帮你填空并猜出问题。

    默认情况下,ASP.NET SynchronizationContext 会流动,但是 the way it flows identity is a bit weird 。它实际上是流动的 HttpContext.Current.User 然后设置 Thread.CurrentPrincipal 除此之外。所以,如果你只是设置 线程当前主体 ,您将看不到它的正确流动。

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

    • 从那时起 线程当前主体 在线程上设置,则该线程将具有相同的主体,直到它重新进入ASP.NET上下文。
    • 当任何线程进入ASP.NET上下文时, 线程当前主体 被清除(因为它被设置为 HttpContext.Current.User(HTTP上下文当前用户) ).
    • 使用螺纹时 外部 ASP.NET上下文,它只保留 线程当前主体 碰巧被设定在上面。

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

    • 前3个都是从线程63同步报告的 CurrentPrincipal 已显式设置,因此它们都具有预期值。
    • 线程77用于恢复 async 方法,从而进入ASP.NET上下文并清除任何 现任委托人 可能已经发生了。
    • 螺纹63用于 ProcessResponse 。它重新进入ASP.NET上下文,清除其 线程当前主体 .
    • 线程65是一个有趣的线程。它在ASP.NET上下文之外运行(在 ContinueWith 没有调度器),所以它只保留 现任委托人 以前碰巧有。我认为 现任委托人 只是早期测试运行遗留下来的。

    更新后的代码发生更改 PostFile 运行第二部分 外部 ASP.NET上下文。所以它选择了线程65,恰好有 现任委托人 设置由于它在ASP.NET上下文之外, 现任委托人 未清除。

    所以,在我看来 ExecutionContext 流动良好。我相信微软已经测试过了 执行上下文 流出wazoo;否则,世界上的每个ASP.NET应用程序都会存在严重的安全漏洞。需要注意的是,在这个代码中 线程当前主体 只是指当前用户的声明,并不代表实际的模拟。

    如果我的猜测是正确的,那么解决方法很简单: SendAsync ,更改此行:

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

    对此:

    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;
    
        2
  •  0
  •   stymie2    5 年前

    我知道重新输入ASP.NET同步上下文会导致Thread.CurrentPrincipal被设置为HttpContext.Current.User。但我仍然没有看到我预期的行为。我没想到每个等待的调用链都会设置Thread.CurrentPrincipal=HttpContext.Current.User。我看到这甚至超出了我启动async/await链的async void事件处理程序。这是其他人看到的行为吗?我原以为链上的调用会使用它们捕获的上下文来继续,但它们显示了可重入的行为。

    我没有在任何等待的电话上使用.ContinuteWait(false)。我们在web.config中有targetFramework=“4.6.1”,其中包括设置UseTaskFriendlySynchronizationContext=true等。第三方API客户端导致异步/等待链底部的可重入行为。