异步方法调用和模拟

时间:2015-07-20 17:26:16

标签: c# asp.net .net asynchronous async-await

为什么模拟用户上下文仅在异步方法调用之前可用? 我编写了一些代码(实际上基于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 pool用户的名字。代码运行的时间。这意味着我不再拥有被模仿的用户上下文。如果延迟更改为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()。

2 个答案:

答案 0 :(得分:13)

在ASP.NET中,与WindowsIdentity不同,AspNetSynchronizationContext不会自动流过Thread.CurrentPrincipal。每次ASP.NET进入新的池线程时,模拟上下文都会被保存并设置为{app}用户的here。当ASP.NET离开线程时,它将被恢复here。对await延续也会发生这种情况,作为延续回调调用的一部分(由AspNetSynchronizationContext.Post排队的那些)。

因此,如果要在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在评论中建议的那样,自动流动模拟上下文的另一个选项是在全局<alwaysFlowImpersonationPolicy enabled="true"/>文件中使用aspnet.config(如果需要,还可以使用{{1}例如,对于<legacyImpersonationPolicy enabled="false"/>)。

答案 1 :(得分:2)

如果通过httpWebRequest

使用模拟的异步http调用,则会出现这种情况
HttpWebResponse webResponse;
            using (identity.Impersonate())
            {
                var webRequest = (HttpWebRequest)WebRequest.Create(url);
                webResponse = (HttpWebResponse)(await webRequest.GetResponseAsync());
            }

还需要在aspnet.config中设置<legacyImpersonationPolicy enabled="false"/>设置。否则,HttpWebRequest将代表app pool用户而不是模拟用户发送。