2015-07-20 89 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將代表應用程序池用戶而不是模擬用戶發送。