2015-07-20 60 views
10

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

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

令我惊讶的是,在这种情况下,我将收到App池用户的名称。代码正在运行。这意味着我不再拥有这个莽撞的用户环境。如果延迟变为0,这使得呼叫同步:

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

代码将返回当前模拟用户的名称。 据我了解await和调试器显示,context.Dispose()不被调用,直到名称被分配。

+2

你已经模拟了一些随机线程池线程。运行它的下一个请求可能会受此影响。超级危险。 – usr

+1

@usr,因为它变成了,除非你冒充类似'UnsafeQueueUserWorkItem'这样的东西,否则它并不是那么危险。否则,身份得到正确的传播和恢复,它不会留在池线程中。看[这个小实验](https://gist.github.com/noserati/940c21b488e59d502dd1),特别是'GoThruThreads'。在ASP.NET中更安全,请检查我的更新。 – Noseratio

+0

@Noseratio很高兴知道。 – usr

回答

12

在ASP.NET中,WindowsIdentity不会自动流动AspNetSynchronizationContext,不像说Thread.CurrentPrincipal。每次ASP.NET进入一个新的线程池时,模拟上下文都会被保存,并将其设置为应用程序池用户的here。当ASP.NET离开该线程时,它将被恢复为here。作为延续回调调用的一部分(由AspNetSynchronizationContext.Post排队的调用),这也会发生在await延续中。因此,如果您希望跨越ASP.NET中多个线程的等待事件来保持身份,则需要手动进行流式处理。你可以使用本地或类成员变量。或者,你可以通过logical call context,.NET 4.6 AsyncLocal<T>或类似的东西Stephen Cleary's AsyncLocal

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

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

(注意:虽然你会在这种情况下失去了HttpContext.Current。)

上面会的工作,因为在没有同步上下文,WindowsIdentity确实流过了await。它几乎流入the same way as Thread.CurrentPrincipal does,即跨越并进入异步调用(但不在这些异步调用之外)。我相信这是作为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中使用 <alwaysFlowImpersonationPolicy enabled="true"/>(如果需要,也可使用 <legacyImpersonationPolicy enabled="false"/>,例如 HttpWebRequest)。

+3

非常感谢您的深入解答。它确实帮助我理解了传入和传出异步调用的上下文背后的原因。我发现了另一种解决方案,可以更改默认行为并允许将身份传递给由异步创建的最终线程。这在这里解释:http://stackoverflow.com/a/10311823/637443。以下设置也适用于使用app.config的应用程序:。使用此配置,身份将被传递并保留。如果不是这样,请纠正。 –

+1

@PawełForys,我认为''应该这样做,你可能不需要'legacyImpersonationPolicy'。让我们知道这是否有效。 – Noseratio

+1

正确,单独允许身份跨线程流动。谢谢! –

2

看来,使用模拟的异步HTTP调用的情况下,通过HttpWebRequest的

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

设定<legacyImpersonationPolicy enabled="false"/>需求也aspnet.config进行设置。否则,HttpWebRequest将代表应用程序池用户而不是模拟用户发送。