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

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

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

我遇到了非常令人困惑和潜在危险的错误。 我注意到我的线程的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 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 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后面的代码调用了我的业务逻辑,或者只需要正确设置安全上下文。 存在潜在的完整性问题。

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

提前致谢。

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

默认情况下,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上下文之外运行其第二部分。 所以它选择了线程65,恰好设置了CurrentPrincipal 。 由于它在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"))); 

对此:

 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;