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

本文关键字:异步操作 ExecutionContext NET ASP Web API 我的 使用 | 更新日期: 2023-09-27 18:09:00

我很难理解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来处理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

让事情变得更混乱,当我在async行后面添加以下内容时:

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语句后面的代码实际上调用了业务逻辑,或者只是要求正确设置安全上下文。存在潜在的完整性问题。

有谁能告诉我发生了什么事吗?

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

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

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

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

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

将此应用到您的原始代码和输出:

  • 前3个都是在线程63的CurrentPrincipal被显式设置后同步报告的,所以它们都有期望值。
  • 线程77用于恢复async方法,从而进入ASP。. NET上下文并清除它可能拥有的任何CurrentPrincipal
  • 线程63用于ProcessResponse。它重新进入ASP。. NET上下文,清除其Thread.CurrentPrincipal .线程65是有趣的一个。它在ASP之外运行。. NET上下文(在没有调度器的ContinueWith中),所以它只保留之前碰巧拥有的CurrentPrincipal。我假设它的CurrentPrincipal只是从早期的测试运行中留下的。

更新后的代码将PostFile更改为在ASP 之外运行其第二部分。净上下文。所以它拿起线程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;

我明白重新进入ASP。. NET同步上下文将导致线程。将CurrentPrincipal设置为HttpContext.Current.User。但我仍然没有看到我所期望的行为。我没有想到每个等待的调用都会设置线程。CurrentPrincipal = HttpContext.Current.User。我看到这甚至超越了async void事件处理程序,我开始async/await链。这是其他人看到的行为吗?我期望调用链使用它们捕获的上下文继续,但它们显示了可重入行为。

我没有在任何等待的调用上使用. continueawait (false)。我们在web中有targetFramework="4.6.1"。它在后台设置UseTaskFriendlySynchronizationContext = true,等等。第三方API客户端导致了async/await链底部的可重入行为。