问题 异步方法调用和模拟


为什么模拟用户上下文仅在异步方法调用之前可用? 我编写了一些代码(实际上基于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()。


12370
2017-07-20 17:26


起源

你已经冒充了一些随机线程池线程。下一个在其上运行的请求可能会受此影响。超级危险。 - usr
@usr,当它转过来时,除非你冒充内心之类的东西,否则它并没有那么危险 UnsafeQueueUserWorkItem。否则,身份会被正确传播和恢复,它不会挂在池线程上。看到 这个小实验特别是 GoThruThreads。它在ASP.NET中更安全,请检查我的更新。 - Noseratio
@Noseratio很高兴知道。 - usr


答案:


在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 流,这本身就是其中的一部分 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 文件(如果需要, <legacyImpersonationPolicy enabled="false"/> 例如,对于 HttpWebRequest)。


12
2017-07-20 21:23



非常感谢您的回答。它真的帮助我理解了异步调用传递的上下文背后的内容。我找到了另一个可以改变默认行为并允许将标识传递给async创建的最终线程的解决方案。这里解释的是: stackoverflow.com/a/10311823/637443。设置:<legacyImpersonationPolicy enabled =“false”/>和<alwaysFlowImpersonationPolicy enabled =“true”/>也适用于使用app.config的应用程序。使用此配置,将传递和保留身份。如果不是这样,请更正。 - Paweł Forys
@PawełForys,我想 <alwaysFlowImpersonationPolicy enabled="true"/> 单独应该这样做,你可能不需要 legacyImpersonationPolicy。如果有效,请告诉我们。 - Noseratio
正确,单独<alwaysFlowImpersonationPolicy enabled =“true”/>允许跨线程的身份流。谢谢! - Paweł Forys
如果您不介意请更新您的答案,正如您所提到的那样“如果您希望在ASP.NET中保持跨越多个线程的身份,则需要手动流动”。使用<alwaysFlowImpersonationPolicy>时,会自动完成。谢谢! - Paweł Forys
@PawełForys,完成了。 - Noseratio


在通过httpWebRequest使用模拟的异步http调用的情况下似乎

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

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


2
2017-07-22 12:08