Асинхронный вызов метода и олицетворение

Почему пользовательский контекст олицетворения доступен только до тех пор, пока не будет вызван метод async? Я написал некоторый код (фактически основанный на веб-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;
}

К моему удивлению, в этой ситуации я получу имя пользователя приложения. под которым работает код. Это означает, что у меня больше нет контекста пользователя. Если задержка изменяется на 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;
}

Код вернет имя текущего пользователя. Насколько я понимаю, ожидание и то, что показывает отладчик, context.Dispose() не вызывается до тех пор, пока не будет назначено имя.

Ответ 1

В ASP.NET WindowsIdentity автоматически не течет AspNetSynchronizationContext, в отличие от Thread.CurrentPrincipal. Каждый раз, когда ASP.NET вводит новый поток пула, контекст олицетворения сохраняется и устанавливает здесь для пользователя пользователя пула приложений. Когда ASP.NET покидает поток, он восстанавливается здесь. Это происходит и для продолжений await, как часть вызовов обратного вызова продолжения (в очереди AspNetSynchronizationContext.Post).

Таким образом, если вы хотите, чтобы идентификатор в ожидании охватывал несколько потоков в ASP.NET, вам необходимо передать его вручную. Для этого вы можете использовать переменную-член класса или класса. Или вы можете передать его через контекст логического вызова, с .NET 4.6 AsyncLocal<T> или что-то вроде Stephen Cleary AsyncLocal.

В качестве альтернативы, ваш код будет работать так, как ожидалось, если вы использовали ConfigureAwait(false):

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

(Обратите внимание, что в этом случае вы потеряете HttpContext.Current.)

Вышеупомянутое будет работать, потому что при отсутствии контекста синхронизации WindowsIdentity действительно течет через await. Он протекает в значительной степени так же, как Thread.CurrentPrincipal делает, то есть через асинхронные вызовы (но не за их пределами). Я считаю, что это делается как часть SecurityContext flow, который сам является частью ExecutionContext и показывает ту же копию -write поведение.

Чтобы поддержать этот оператор, я немного экспериментировал с консольным приложением :

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 в комментариях, другой вариант потока контекста олицетворения автоматически заключается в использовании <alwaysFlowImpersonationPolicy enabled="true"/> в глобальном файле aspnet.config (и, при необходимости, <legacyImpersonationPolicy enabled="false"/>, например, например для HttpWebRequest).

Ответ 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 отправит от имени пользователя пула приложений и не выдает себя за пользователя.