使用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 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链底部的可重入行为。