异步方法调用和模拟身份验证

13

为什么模拟用户上下文只在异步方法调用期间可用?我编写了一些代码(实际上基于 Web API),以检查模拟的用户上下文的行为。

为什么只有在异步方法调用期间才能使用模拟用户上下文呢?我编写了一些代码(实际上是基于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;
}

令我惊讶的是,在这种情况下,我将接收应用程序池用户的名称。代码正在其下运行。这意味着我不再拥有模拟用户上下文。如果将延迟更改为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
你已经在一些随机线程池线程上冒充了身份。接下来要在其上运行的请求可能会受到影响。非常危险。 - usr
1
@usr,事实证明,除非你在类似于“UnsafeQueueUserWorkItem”之类的东西内部进行了模拟,否则它并不危险。否则,身份将得到正确传播和恢复,并不会留在池线程上挂起。请参见这个小实验,尤其是“GoThruThreads”。在ASP.NET中甚至更加安全,请查看我的更新。 - noseratio - open to work
@Noseratio 很好知道。 - usr
2个回答

14
在ASP.NET中,与Thread.CurrentPrincipal不同,WindowsIdentity不会自动流动到AspNetSynchronizationContext。每次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);
        }
    }
}


3
非常感谢你提供深入的回答,它确实帮助我理解了异步调用所传递上下文背后的内容。我已经找到另一种解决方案,可以更改默认行为,并允许将标识传递给异步创建的最终线程。具体解释见此链接:https://dev59.com/mGkv5IYBdhLWcg3w1kWr#10311823。在应用程序使用 app.config 的情况下,设置:<legacyImpersonationPolicy enabled="false"/> 和 <alwaysFlowImpersonationPolicy enabled="true"/> 也应该有效。使用这个配置,标识将被传递和保留。如果不正确,请纠正我。 - Paweł Forys
1
@PawełForys,我认为<alwaysFlowImpersonationPolicy enabled="true"/>就足够了,你可能不需要legacyImpersonationPolicy。如果有用,请告诉我们。 - noseratio - open to work
1
正确的,独立的 <alwaysFlowImpersonationPolicy enabled="true"/> 允许身份在线程之间流动。谢谢! - Paweł Forys
1
如果您不介意的话,请更新您的回答,因为您提到“如果要跨越ASP.NET的多个线程保持身份识别,则需要手动进行流操作”。当使用<alwaysFlowImpersonationPolicy>时,这将自动完成。谢谢! - Paweł Forys
1
启用alwaysFlowImpersonationPolicy会有任何安全风险需要注意吗? - eaglei22
显示剩余3条评论

2

看起来,如果使用httpWebRequest进行模拟异步http调用

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

设置<legacyImpersonationPolicy enabled="false"/>还需要在aspnet.config文件中进行设置。否则,HttpWebRequest将代表应用程序池用户而不是模拟用户发送请求。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接