问题描述
根据我在网上阅读的内容,上下文相关的项目,例如安全性(线程主体)、文化等,应该在工作执行单元的范围内跨异步线程流动.
From what I've read online, context-sensitive items such as security (Thread Principal), culture, etc, should flow across asynchronous threads within the bounds of an execution unit of work.
不过,我遇到了非常令人困惑和潜在危险的错误.我注意到我的线程的 CurrentPrincipal 在异步执行过程中丢失了.
I'm encountering very confusing and potentially dangerous bugs though. I'm noticing my thread's CurrentPrincipal is getting lost across async execution.
这是一个 ASP.NET Web API 场景示例:
Here is an example ASP.NET Web API scenario:
首先,让我们设置一个简单的 Web API 配置,其中包含两个委托处理程序以进行测试.
First, let's setup a simple Web API configuration with two delegating handlers for testing purposes.
他们所做的只是写出调试信息并传递请求/响应,除了第一个DummyHandler"设置线程的主体以及要跨上下文共享的一段数据(请求的相关 ID).
All they do is write out debug information and pass the request/response on through, except the first "DummyHandler" which sets the thread's principal as well as a piece of data to be shared across the context (the request's correlation 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,就像您在上传文件一样.
Simple enough. Next let's add a single ApiController to handle an HTTP POST, as if you were uploading files.
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 emp"));
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 运行测试后,这是我收到的输出:
Upon running a test with Fiddler, this is the output I receive:
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
为了让事情更加混乱,当我将以下内容附加到异步行时:
To make matters more confusing, When I append the follow to the async line:
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 my 后面的代码实际上调用了我的业务逻辑,或者只是要求正确设置安全上下文.存在潜在的完整性问题.
The point here is this. The code following the async my in fact call my business logic or simply require the security context be properly set. There is a potential integrity problem going on.
谁能帮忙解释一下发生了什么?
Can anyone help shed some light one what is happening?
提前致谢.
推荐答案
我没有所有的答案,但我可以帮助填空并猜测问题.
I don't have all the answers, but I can help fill in some blanks and guess at the problem.
默认情况下,ASP.NET SynchronizationContext
将流动,但 它流动身份的方式有点奇怪.它实际上流 HttpContext.Current.User
然后将 Thread.CurrentPrincipal
设置为那个.所以如果你只是设置Thread.CurrentPrincipal
,你不会看到它正确地流动.
By default, the ASP.NET SynchronizationContext
will flow, but the way it flows identity is a bit weird. It actually flows HttpContext.Current.User
and then sets Thread.CurrentPrincipal
to that. So if you just set Thread.CurrentPrincipal
, you won't see it flow correctly.
实际上,您会看到以下行为:
In fact, you'll see the following behavior:
- 从在线程上设置
Thread.CurrentPrincipal
起,该线程将具有相同的主体,直到它重新进入 ASP.NET 上下文. - 当任何线程进入 ASP.NET 上下文时,
Thread.CurrentPrincipal
被清除(因为它被设置为HttpContext.Current.User
). - 当在 ASP.NET 上下文之外使用线程时,它只会保留任何
Thread.CurrentPrincipal
碰巧在其上设置的内容.
- From the time
Thread.CurrentPrincipal
is set on a thread, that thread will have that same principal until it re-enters an ASP.NET context. - When any thread enters the ASP.NET context,
Thread.CurrentPrincipal
is cleared (because it's set toHttpContext.Current.User
). - When a thread is used outside the ASP.NET context, it just retains whatever
Thread.CurrentPrincipal
happened to be set on it.
将此应用于您的原始代码和输出:
Applying this to your original code and output:
- 前 3 个都是在其
CurrentPrincipal
显式设置后从线程 63 同步报告的,因此它们都具有预期值. - 线程 77 用于恢复
async
方法,从而进入 ASP.NET 上下文并清除它可能拥有的任何CurrentPrincipal
. - 线程 63 用于
ProcessResponse
.它重新进入 ASP.NET 上下文,清除其Thread.CurrentPrincipal
. - 线程 65 很有趣.它在 ASP.NET 上下文之外运行(在没有调度程序的
ContinueWith
中),所以它只保留它之前碰巧拥有的任何CurrentPrincipal
.我假设它的CurrentPrincipal
只是从早期的测试运行中遗留下来的.
- The first 3 are all reported synchronously from thread 63 after its
CurrentPrincipal
was explicitly set, so they all have the expected value. - Thread 77 is used to resume the
async
method, thus entering the ASP.NET context and clearing anyCurrentPrincipal
it may have had. - Thread 63 is used for
ProcessResponse
. It re-enters the ASP.NET context, clearing itsThread.CurrentPrincipal
. - Thread 65 is the interesting one. It is running outside the ASP.NET context (in a
ContinueWith
without a scheduler), so it just retains whateverCurrentPrincipal
it happened to have before. I assume that itsCurrentPrincipal
is just left over from an earlier test run.
更新后的代码更改了 PostFile
以在 ASP.NET 上下文外部运行其第二部分.所以它选择线程 65,它恰好设置了 CurrentPrincipal
.由于它在 ASP.NET 上下文之外,CurrentPrincipal
未被清除.
The updated code changes PostFile
to run its second portion outside the ASP.NET context. So it picks up thread 65, which just happens to have CurrentPrincipal
set. Since it's outside the ASP.NET context, CurrentPrincipal
isn't cleared.
所以,在我看来 ExecutionContext
运行良好.我确信微软已经测试了 ExecutionContext
流出来的 wazoo;否则世界上的每个 ASP.NET 应用程序都会有严重的安全漏洞.需要注意的是,在这段代码中,Thread.CurrentPrincipal
只是指当前用户的声明,并不代表实际的冒充.
So, it looks to me like ExecutionContext
is flowing fine. I'm sure Microsoft has tested ExecutionContext
flow out the wazoo; otherwise every ASP.NET app in the world would have a serious security flaw. It's important to note that in this code Thread.CurrentPrincipal
just refers to the current user's claims and does not represent actual impersonation.
如果我的猜测是正确的,那么修复很简单:在 SendAsync
中,更改这一行:
If my guesses are correct, then the fix is quite simple: in SendAsync
, change this line:
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 Web API,我的 ExecutionContext 不在异步操作中流动的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!