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
).
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
@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
@Noseratio dobrze wiedzieć. – usr