使用ASP.NET Web API时,我的ExecutionContext在异步操作中无法流动。

23

我对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;
    }
}

非常简单。接下来,我们添加一个单独的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);
        }
    }
}

通过使用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

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

有人可以帮忙解释一下发生了什么吗?

提前致谢。

2个回答

30

我不是所有问题的答案,但我可以帮助填补一些空白并猜测问题所在。

默认情况下,ASP.NET SynchronizationContext 会流动,但它流动身份的方式有点奇怪。它实际上会流动 HttpContext.Current.User,然后将 Thread.CurrentPrincipal 设置为那个用户。因此,如果您只设置了 Thread.CurrentPrincipal,则无法正确地看到它流动。

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

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

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

  • 前三个都是在线程63显式设置了CurrentPrincipal后同步报告的,所以它们都具有预期值。
  • 线程77用于恢复async方法,因此进入ASP.NET上下文并清除可能拥有的任何CurrentPrincipal
  • 线程63用于ProcessResponse。 它重新进入ASP.NET上下文,清除其Thread.CurrentPrincipal
  • 线程65是有趣的。它在ASP.NET上下文之外运行(在没有调度程序的ContinueWith中),因此它只保留它之前拥有的任何CurrentPrincipal。 我假设它的CurrentPrincipal仅剩下早期测试运行的结果。

更新的代码将PostFile更改为在ASP.NET上下文之外运行其第二部分。 因此,它选择线程65,该线程恰好具有CurrentPrincipal。由于它在ASP.NET上下文之外,CurrentPrincipal不会被清除。

因此,我认为ExecutionContext流程良好。我相信微软已经对ExecutionContext流程进行了大量测试;否则,世界上每个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")));

转换为:

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
Stephen,我无法感谢你的清晰明了的回答。我曾经阅读过你之前在SO上回答有关ExecutionContext的问题,以及你和Stephen Toub在这个主题上的博客。但仍然无法确定我的问题原因。再次感谢!你的修复方法完美解决了我的问题。 - Daniel
很高兴能够帮忙!关键是Scott Hanselman的博客,他展示了ASP.NET身份流动有点奇怪。 - Stephen Cleary
你好@stephencleary, 我注意到在使用IHttpModule(BeginRequest)中的LogicalSetData(“ecid”,Guid.NewGuid())时,ExecutionContext流程存在“不一致性”。对于所有情况,我都可以从ApiController访问“ecid”,除非请求恰好是表单多部分POST。实际上,我想做的是统一应用程序管道的某些部分,每个请求缓存项目,因此我需要创建请求标识符(ecid)。有什么想法为什么形式多部分会以不同的方式/流动?我还看到了C# 4/5之间的不同。 - Daniel
请确保设置UseTaskFriendlySynchronizationContext并且目标为.NET 4.5。WebAPI在技术上可以在.NET 4.0中工作,但我对这种设置非常谨慎(甚至不确定是否支持)。如果您仍然遇到问题,请发布一个新的问题(如果可能,请提供最小的重现代码)。 - Stephen Cleary
@StephenCleary,请问您能否帮我回答这个问题:https://dev59.com/aIDba4cB1Zd3GeqPHKpp#24235652 - eddy

0

我知道重新进入ASP.NET同步上下文会导致Thread.CurrentPrincipal被设置为HttpContext.Current.User。但是我仍然没有看到我期望的行为。我没有预料到每个awaited调用都会将Thread.CurrentPrincipal = HttpContext.Current.User。我甚至在异步void事件处理程序中看到了这一点,这是我启动异步/等待链的地方。其他人是否也看到了这种行为?我希望调用链向上使用它们捕获的上下文继续,但它们显示出了可重入行为。

我在任何等待的调用上都没有使用.ContinueAwait(false)。我们在web.config中有targetFramework="4.6.1",在底层设置了UseTaskFriendlySynchronizationContext = true,以及其他一些东西。第三方API客户端在异步/等待链的底部引起了可重入行为。


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