异步方法调用和模拟

Paw*_*rys 11 .net c# asp.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;
}
Run Code Online (Sandbox Code Playgroud)

令我惊讶的是,在这种情况下,我将收到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;
}
Run Code Online (Sandbox Code Playgroud)

代码将返回当前模拟用户的名称.据我所知,等待和调试器显示的内容,在分配名称之前不会调用context.Dispose().

nos*_*tio 13

在ASP.NET中,WindowsIdentity不会自动流过AspNetSynchronizationContext,不像说Thread.CurrentPrincipal.每次ASP.NET进入新的池线程时,模拟上下文都会被保存并在此处设置为应用程序池用户的模拟上下文.当ASP.NET离开线程时,它会在此处恢复.对于awaitcontinuation也会发生这种情况,作为continuation回调调用(排队的那些AspNetSynchronizationContext.Post)的一部分.

因此,如果要在ASP.NET中保持跨越多个线程的身份,则需要手动流动它.您可以使用本地或类成员变量.或者,您可以通过逻辑调用上下文,使用.NET 4.6 AsyncLocal<T>或类似Stephen Cleary的内容来传递它AsyncLocal.

或者,如果您使用ConfigureAwait(false)以下代码,您的代码将按预期工作:

await Task.Delay(1).ConfigureAwait(false);
Run Code Online (Sandbox Code Playgroud)

(注意,虽然HttpContext.Current在这种情况下你输了.)

上述方法可行,因为在没有同步上下文的情况下,WindowsIdentity确实会流过await.它流动在几乎相同的方式,Thread.CurrentPrincipal确实,即跨越,进入异步调用(而不是之外的).我相信这是作为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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)


更新,正如@PawelForys在评论中建议的那样,自动流动模拟上下文的另一个选项是<alwaysFlowImpersonationPolicy enabled="true"/>在全局aspnet.config文件中使用(如果需要,<legacyImpersonationPolicy enabled="false"/>也可以在例如中使用HttpWebRequest).

  • 非常感谢您的回答.它真的帮助我理解了异步调用传递的上下文背后的内容.我找到了另一个可以改变默认行为并允许将标识传递给async创建的最终线程的解决方案.这里有解释:http://stackoverflow.com/a/10311823/637443.设置:<legacyImpersonationPolicy enabled ="false"/>和<alwaysFlowImpersonationPolicy enabled ="true"/>也适用于使用app.config的应用程序.使用此配置,将传递和保留标识.如果不是这样,请更正. (3认同)