.NET Framework Web API中WindowsIdentity.RunImpersonated跨Task.Run丢失身份模拟上下文
问题场景
传统.NET Framework Web API需以模拟身份调用独立维护的.NET库,库通过返回Task的方法处理约1秒的耗时操作(库本身不感知身份模拟逻辑,由Web API决定模拟时机)。但执行过程中发现,库内调用Task.Run时,身份模拟上下文会丢失——预期Inside Task.Run行显示模拟用户DOMAIN\userone,实际却显示应用池账户DOMAIN\app_pool。
复现代码如下:
public class ValuesController : ApiController { // GET api/values public async Task<IEnumerable<string>> Get() { List<string> logs = new List<string>(); logs.Add($"Before impersonation: {WindowsIdentity.GetCurrent().Name} on thread {Thread.CurrentThread.ManagedThreadId}"); IntPtr logonToken = ((WindowsIdentity)User.Identity).Token; int result = await WindowsIdentity.RunImpersonated(new Microsoft.Win32.SafeHandles.SafeAccessTokenHandle(logonToken), async () => { logs.Add($"During impersonation: {WindowsIdentity.GetCurrent().Name} on thread {Thread.CurrentThread.ManagedThreadId}"); Task<int> taskFromWorkerLibrary = Task.Run(() => { logs.Add($"Inside Task.Run: {WindowsIdentity.GetCurrent().Name} on thread {Thread.CurrentThread.ManagedThreadId}"); Thread.Sleep(3000); // 模拟长耗时操作 return new Random().Next(); // 区分不同调用结果 }); return await taskFromWorkerLibrary; }); logs.Add($"Result: {result} on thread {Thread.CurrentThread.ManagedThreadId}"); logs.Add($"After impersonation: {WindowsIdentity.GetCurrent().Name} on thread {Thread.CurrentThread.ManagedThreadId}"); return logs.ToArray(); } }
实际输出:
Before impersonation: DOMAIN\app_pool on thread 33 During impersonation: DOMAIN\userone on thread 33 Inside Task.Run: DOMAIN\app_pool on thread 31 Result: 1625421834 on thread 35 After impersonation: DOMAIN\app_pool on thread 35
注:相同代码在ASP.NET Core Web API中可正常保留Task.Run内的模拟上下文。
问题原因
传统ASP.NET使用AspNetSynchronizationContext,该同步上下文不会自动将Windows身份模拟的上下文传递到Task.Run所开启的线程池线程;而ASP.NET Core采用简化的同步上下文模型(或默认不使用自定义同步上下文),身份模拟上下文会随异步流程自动流动。此外,WindowsIdentity.RunImpersonated的异步委托中,当代码通过Task.Run脱离当前同步上下文时,模拟身份不会被自动携带到新线程。
解决方案
方案1:移除不必要的Task.Run调用
如果库中的耗时操作本身可以以异步方式实现(或无需强制切换线程),直接调用异步方法而非用Task.Run包装。Task.Run在这里仅会强制切换线程,不仅无意义还会导致上下文丢失。
方案2:在Task.Run内部重新应用身份模拟
在Task.Run的委托内,重新使用模拟令牌执行身份模拟操作,确保新线程上的代码运行在模拟上下文内:
// 在RunImpersonated内部获取有效令牌 IntPtr impersonatedToken = WindowsIdentity.GetCurrent().Token; Task<int> taskFromWorkerLibrary = Task.Run(() => { using (WindowsIdentity.Impersonate(impersonatedToken)) { logs.Add($"Inside Task.Run: {WindowsIdentity.GetCurrent().Name} on thread {Thread.CurrentThread.ManagedThreadId}"); Thread.Sleep(3000); return new Random().Next(); } });
方案3:全局配置身份模拟上下文流动
在Web API的web.config中添加配置,开启身份模拟上下文的自动流动,让线程池线程自动继承当前模拟身份:
<configuration> <runtime> <legacyImpersonationPolicy enabled="false" /> <alwaysFlowImpersonationPolicy enabled="true" /> </runtime> </configuration>
该配置会改变整个应用的身份流动行为,需评估对其他功能的影响。
总结
- 优先选择方案1,移除不必要的线程切换,从根源避免上下文丢失问题;
- 若无法修改库的实现,方案2是针对性的局部修复;
- 方案3为全局配置,适合需要全应用范围身份流动的场景,但需注意兼容性。
内容的提问来源于stack exchange,提问作者jproch

