2015-07-20 14 views
10

Dlaczego kontekst użytkownika personifikacji jest dostępny tylko do czasu wywołania metody asynchronicznej? Napisałem kod (w rzeczywistości oparty na Web API), aby sprawdzić zachowanie podszytego kontekstu użytkownika.Wywołanie metody asynchronicznej i podszywanie się pod inną osobę

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; 
} 

Ku mojemu zaskoczeniu w tej sytuacji otrzymam nazwę użytkownika puli aplikacji. pod którym działa kod. Oznacza to, że nie mam już kontekstu imprsonated user. Jeśli opóźnienie zostanie zmienione na 0, co spowoduje synchroniczne wywołanie:

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; 
} 

Kod zwróci nazwę aktualnie podanego użytkownika. O ile rozumiem, oczekuję i co pokazuje debugger, context.Dispose() nie jest wywoływana, dopóki nazwa nie jest przypisana.

+2

Podałeś się na jakimś losowym wątku puli wątków. Może to mieć wpływ na kolejne żądanie uruchomienia. Super niebezpieczne. – usr

+1

@usr, jak się okazuje, to nie jest tak niebezpieczne, chyba że podszywasz się pod coś takiego jak 'UnsafeQueueUserWorkItem'. W przeciwnym razie tożsamość zostanie poprawnie propagowana i przywrócona, nie zostanie zawieszona na wątku puli. Zobacz [ten mały eksperyment] (https://gist.github.com/noserati/940c21b488e59d502dd1), w szczególności "GoThruThreads". W ASP.NET jest jeszcze bezpieczniej, sprawdź moją aktualizację. – Noseratio

+0

@Noseratio dobrze wiedzieć. – usr

Odpowiedz

12

W środowisku ASP.NET WindowsIdentity nie otrzymuje automatycznie przepływu przez AspNetSynchronizationContext, w przeciwieństwie do powiedzmy Thread.CurrentPrincipal. Za każdym razem, gdy program ASP.NET wprowadza nowy wątek puli, kontekst personifikacji zostaje zapisany i ustawiony jako here na identyfikator użytkownika puli aplikacji. Kiedy ASP.NET opuszcza wątek, zostaje przywrócony here. Dzieje się tak również w przypadku kontynuacji await w ramach wywołań wywołania zwrotnego kontynuacji (w kolejce do AspNetSynchronizationContext.Post).

Tak więc, jeśli chcesz zachować tożsamość w oczekiwaniu na wiele wątków w ASP.NET, musisz przepłynąć ręcznie. Możesz użyć do tego zmiennej lokalnej lub członkowskiej klasy. Lub możesz przepłynąć przez logical call context, z .NET 4.6 AsyncLocal<T> lub coś w rodzaju Stephen Cleary's AsyncLocal.

Alternatywnie, Twój kod będzie działać zgodnie z oczekiwaniami, jeśli użyto ConfigureAwait(false):

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

(Zauważ jednak, że można stracić HttpContext.Current w tym przypadku).

Powyższy będzie działać, bo w nieobecność kontekstu synchronizacji, WindowsIdentity powoduje przepływ przez await. Przepływa w przybliżeniu the same way as Thread.CurrentPrincipal does, tj. Przez i do asynchronicznych wywołań (ale nie poza nimi). Uważam, że dzieje się tak w ramach przepływu SecurityContext, który sam w sobie jest częścią ExecutionContext i pokazuje to samo zachowanie przy zapisie na piśmie.

Aby wesprzeć to stwierdzenie, zrobiłem mały eksperyment z aplikacją konsoli:

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); 
     } 
    } 
} 


Updated, jak @PawelForys sugeruje w komentarzach, inna opcja płynąć kontekst personifikacji jest automatycznie używać <alwaysFlowImpersonationPolicy enabled="true"/> w globalnym pliku aspnet.config (i, jeśli to konieczne, <legacyImpersonationPolicy enabled="false"/>, jak również, na przykład dla HttpWebRequest).

+3

Dziękuję bardzo za głęboką odpowiedź. To naprawdę pomogło mi zrozumieć, co pozostaje za kontekstem przekazywanym do iz połączeń asynchronicznych. Znalazłem inne rozwiązanie, które może zmienić domyślne zachowanie i umożliwić przekazanie tożsamości do ewentualnych wątków stworzonych przez asynchronizację. Które jest wyjaśnione tutaj: http://stackoverflow.com/a/10311823/637443. Ustawienia: i powinny również działać dla aplikacji używających app.config. Przy użyciu tej konfiguracji tożsamość zostanie przekazana i zachowana. Proszę poprawić, jeśli to nie jest prawda. –

+1

@ PawełForys, myślę, że '' alwaysFlowImpersonationPolicy enabled = "true" /> 'sam powinien to zrobić, prawdopodobnie nie potrzebujesz' legacyImpersonationPolicy'. Daj nam znać, jeśli to działa. – Noseratio

+1

Prawidłowe, samodzielne umożliwia przepływ tożsamości między wątkami. Dzięki! –

2

Wydaje się, że w przypadku korzystania z wcieliła async połączeń HTTP przez HttpWebRequest

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

na ustawienie <legacyImpersonationPolicy enabled="false"/> musi również być ustawiony w aspnet.config. W przeciwnym razie HttpWebRequest będzie wysyłać w imieniu użytkownika puli aplikacji, a nie podszywać się pod użytkownika.

Powiązane problemy