异步方法调用和模拟

为什么模拟用户上下文仅在异步方法调用之前可用? 我编写了一些代码(实际上基于Web API)来检查模拟用户上下文的行为。

async Task Test() { var context = ((WindowsIdentity)HttpContext.Current.User.Identity).Impersonate(); await Task.Delay(1); var name = WindowsIdentity.GetCurrent().Name; context.Dispose(); return name; } 

令我惊讶的是,在这种情况下,我将收到App pool用户的名字。 代码运行的位置。 这意味着我不再拥有被模仿的用户上下文。 如果延迟更改为0,则使调用同步:

 async Task Test() { var context = ((WindowsIdentity)HttpContext.Current.User.Identity).Impersonate(); await Task.Delay(0); var name = WindowsIdentity.GetCurrent().Name; context.Dispose(); return name; } 

代码将返回当前模拟用户的名称。 据我所知,等待和调试器显示的内容,在分配名称之前不会调用context.Dispose()。

在ASP.NET中,与Thread.CurrentPrincipal不同, WindowsIdentity不会被AspNetSynchronizationContext自动流动。 每次ASP.NET进入新的池线程时,模拟上下文都会被保存并在此处设置为应用程序池用户的模拟上下文。 当ASP.NET离开线程时,它将在此处恢复。 这也是为了await延续,作为延续回调调用的一部分(由AspNetSynchronizationContext.Post排队的AspNetSynchronizationContext.Post )。

因此,如果要在ASP.NET中保持跨越多个线程的身份,则需要手动流动它。 您可以使用本地或类成员变量。 或者,您可以通过逻辑调用上下文 ,使用.NET 4.6 AsyncLocal或类似Stephen Cleary的AsyncLocal

或者,如果您使用ConfigureAwait(false) ,您的代码将按预期工作:

 await Task.Delay(1).ConfigureAwait(false); 

(注意,虽然在这种情况下你会丢失HttpContext.Current 。)

上述方法可行,因为在没有同步上下文的情况下, WindowsIdentity确实会在await流动 。 它的流程与Thread.CurrentPrincipal ,即跨越和进入异步调用(但不在那些之外)。 我相信这是作为SecurityContext流的一部分完成的,它本身是ExecutionContext的一部分,并显示相同的写时复制行为。

为了支持这个陈述,我做了一个控制台应用程序的小实验:

 using System; using System.Diagnostics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Security; using System.Security.Principal; using System.Threading; using System.Threading.Tasks; namespace ConsoleApplication { class Program { static async Task TestAsync() { ShowIdentity(); // substitute your actual test credentials using (ImpersonateIdentity( userName: "TestUser1", domain: "TestDomain", password: "TestPassword1")) { ShowIdentity(); await Task.Run(() => { Thread.Sleep(100); ShowIdentity(); ImpersonateIdentity(userName: "TestUser2", domain: "TestDomain", password: "TestPassword2"); ShowIdentity(); }).ConfigureAwait(false); ShowIdentity(); } ShowIdentity(); } static WindowsImpersonationContext ImpersonateIdentity(string userName, string domain, string password) { var userToken = IntPtr.Zero; var success = NativeMethods.LogonUser( userName, domain, password, (int)NativeMethods.LogonType.LOGON32_LOGON_INTERACTIVE, (int)NativeMethods.LogonProvider.LOGON32_PROVIDER_DEFAULT, out userToken); if (!success) { throw new SecurityException("Logon user failed"); } try { return WindowsIdentity.Impersonate(userToken); } finally { NativeMethods.CloseHandle(userToken); } } static void Main(string[] args) { TestAsync().Wait(); Console.ReadLine(); } static void ShowIdentity( [CallerMemberName] string callerName = "", [CallerLineNumber] int lineNumber = -1, [CallerFilePath] string filePath = "") { // format the output so I can double-click it in the Debuger output window Debug.WriteLine("{0}({1}): {2}", filePath, lineNumber, new { Environment.CurrentManagedThreadId, WindowsIdentity.GetCurrent().Name }); } static class NativeMethods { public enum LogonType { LOGON32_LOGON_INTERACTIVE = 2, LOGON32_LOGON_NETWORK = 3, LOGON32_LOGON_BATCH = 4, LOGON32_LOGON_SERVICE = 5, LOGON32_LOGON_UNLOCK = 7, LOGON32_LOGON_NETWORK_CLEARTEXT = 8, LOGON32_LOGON_NEW_CREDENTIALS = 9 }; public enum LogonProvider { LOGON32_PROVIDER_DEFAULT = 0, LOGON32_PROVIDER_WINNT35 = 1, LOGON32_PROVIDER_WINNT40 = 2, LOGON32_PROVIDER_WINNT50 = 3 }; public enum ImpersonationLevel { SecurityAnonymous = 0, SecurityIdentification = 1, SecurityImpersonation = 2, SecurityDelegation = 3 } [DllImport("advapi32.dll", SetLastError = true)] public static extern bool LogonUser( string lpszUsername, string lpszDomain, string lpszPassword, int dwLogonType, int dwLogonProvider, out IntPtr phToken); [DllImport("kernel32.dll", SetLastError=true)] public static extern bool CloseHandle(IntPtr hObject); } } } 

更新 ,正如@PawelForys在评论中建议的,自动流动模拟上下文的另一个选项是在全局aspnet.config文件中使用 (如果需要, 以及,例如对于HttpWebRequest )。

在通过httpWebRequest使用模拟的异步http调用的情况下似乎

 HttpWebResponse webResponse; using (identity.Impersonate()) { var webRequest = (HttpWebRequest)WebRequest.Create(url); webResponse = (HttpWebResponse)(await webRequest.GetResponseAsync()); } 

设置也需要在aspnet.config中设置。 否则,HttpWebRequest将代表app pool用户而不是模拟用户发送。