22

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

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

任何人都可以帮助阐明正在发生的事情吗?

提前致谢。

4

2 回答 2

29

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

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

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

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

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

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

更新后的代码更改为在 ASP.NET 上下文之外PostFile运行其第二部分。所以它选择线程 65,它恰好已经设置。由于它在 ASP.NET 上下文之外,因此不会被清除。CurrentPrincipalCurrentPrincipal

所以,在我看来,它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")));

对此:

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;
于 2013-04-12T13:10:18.787 回答
0

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

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

于 2019-06-19T17:36:10.490 回答