1. 程式人生 > >從執行上下文角度重新理解.NET(Core)的多執行緒程式設計[3]:安全上下文

從執行上下文角度重新理解.NET(Core)的多執行緒程式設計[3]:安全上下文

在前兩篇文章(《基於呼叫鏈的”引數”傳遞》和《同步上下文》)中,我們先後介紹了CallContext(IllogicalCallContext和LogicalCallContext)、AsyncLocal<T>和SynchronizationContext,它們都是執行緒執行上下文的一部分。本篇介紹的安全上下文(SecurityContext)同樣是執行上下文的一部分,它攜帶了的身份和許可權相關的資訊決定了執行程式碼擁有的控制權限。

目錄
一、SecurityContext
二、讓程式碼在指定Windows賬號下執行
三、抑制模擬賬號的跨執行緒傳播
四、利用Impersonation機制讀取檔案

一、SecurityContext

SecurityContext型別表示的安全上下文主要攜帶兩種型別的安全資訊,一種是通過WindowsIdentity物件表示Windows認證身份,體現為SecurityContext型別的WindowsIdentity屬性。如果採用Windows認證和授權,這個WindowsIdentity物件決定了當前程式碼具有的許可權。SecurityContext型別的另一個屬性返回的CompressedStack攜帶了呼叫堆疊上關於CAS(Code Access Security)相關的資訊。。

public sealed class SecurityContext : IDisposable
{   
    ...
    internal WindowsIdentity WindowsIdentity { get; set; }
    internal CompressedStack CompressedStack { get; set; }
}

由於CAS在.NET Core和.NET 5中不再被支援,所以我們不打算對此展開討論,所以本篇文章討論的核心就是SecurityContext的WindowsIdentity屬性返回的WindowsIdentity物件,這個物件與一種被稱為Impersonation的安全機制。

二、讓程式碼在指定Windows賬號下執行

Windows程序總是在一個指定賬號下執行,該賬號決定了當前程序訪問Windows資源(比如Windows檔案系統)的許可權。安全起見,我們一般會選擇一個許可權較低的賬號(比如Network Service)。如果某些程式碼涉及的資源訪問需要更高的許可權,我們可以針對當前登入使用者對應的Windows賬號(如果採用Windows認證)或者是任意指定的Windows賬號建立一個上下文,在此上下文中的代相當於在指定的Windows賬號下執行,自然擁有了對應賬號的許可權。這種策略相當於模擬/假冒了(Impersonate)了指定賬號執行了某種操作,所以我們將這種機制稱為Impersonation。

我們通過一個簡單的例子來演示一下Impersonation機制。我們首先編寫了如下這個GetWindowsIdentity方法根據指定的賬號和密碼建立對應的WindowsIdentity物件。如程式碼片段所示,方法利用指定的使用者名稱和密碼呼叫了Win31函式LogonUser實施了登入操作,並領用返回的token建立程式碼登入使用者的WindowsIdentity物件。

[DllImport("advapi32.dll")]
public static extern int LogonUser(string lpszUserName,
    string lpszDomain,
    string lpszPassword,
    int dwLogonType,
    int dwLogonProvider,
    ref IntPtr phToken);

public static WindowsIdentity GetWindowsIdentity(string username, string password)
{
    IntPtr token = IntPtr.Zero;
    var status = LogonUser(username, Environment.MachineName, password, 2, 0, ref token);
    if (status != 0)
    {
        return new WindowsIdentity(token);
    }
    throw new InvalidProgramException("Invalid user name or password");
}

我們編寫了如下的程式碼來演示不同執行上下文中當前的Windows賬號是什麼,當前Windows賬號對應的WindowsIdentity物件通過呼叫WindowsIdentity型別的靜態方法GetCurrent獲得。如程式碼片段所示,我們在程式初始化時打印出當前Windows賬號。然後針對賬號foobar(XU\foobar)建立了對應的模擬上下文(Impersonation Context),並在此上下文中打印出當前Windows賬號。我們在模擬上下文中通過建立一個執行緒的方式執行了一個非同步操作,並在非同步執行緒中在此輸出當前Windows賬號。在模擬上下文終結之後,我們在此輸出當前的Windows賬號看看是否恢復到最初的狀態。

class Program
{

    static void Main()
    {
        Console.WriteLine("Before impersonating: {0}", WindowsIdentity.GetCurrent().Name);
        using (GetWindowsIdentity(@"foobar", "password").Impersonate())
        {
            Console.WriteLine("Within Impersonation context: {0}", WindowsIdentity.GetCurrent().Name);
            new Thread(() => Console.WriteLine("Async thread: {0}", WindowsIdentity.GetCurrent().Name)).Start();
        }
        Console.WriteLine("Undo impersonation: {0}", WindowsIdentity.GetCurrent().Name);
        Console.Read();
    }
}

程式執行之後,控制檯上會輸出如下所示的結果。可以看出在預設情況下,模擬的Windows賬號不僅在當前執行緒中有效,還會自動傳遞到非同步執行緒中。

三、抑制模擬賬號的跨執行緒傳播

通過上面的例項我們可以看出在預設情況下安全上下文攜帶的模擬Windows賬號支援跨執行緒傳播,但是有的時候這個機制是不必要的,甚至會程式碼安全隱患,在此情況下我們可以按照如下的當時呼叫SecurityContext的

class Program
{

    static void Main()
    {
        Console.WriteLine("Before impersonating: {0}", WindowsIdentity.GetCurrent().Name);
            using (GetWindowsIdentity(@"foobar", "password").Impersonate())
            {
                SecurityContext.SuppressFlowWindowsIdentity();
                Console.WriteLine("Within Impersonation context: {0}", WindowsIdentity.GetCurrent().Name);
                new Thread(() => Console.WriteLine("Async thread: {0}", WindowsIdentity.GetCurrent().Name)).Start();
            }
        Console.WriteLine("Undo impersonation: {0}", WindowsIdentity.GetCurrent().Name);
        Console.Read();
    }
}

再次執行修改後的程式會得到如下所示的輸出結果,可以看出模擬的Windows賬號(XU\foobar)並沒有傳遞到非同步執行緒中。

四、利用Impersonation機制讀取檔案

訪問當前賬號無權訪問的資源是Impersonation機制的主要應用場景,接下來我們就來演示一下基於檔案訪問的Impersonation應用場景。我們建立了一個文字檔案d:\test.txt,並對其ACL進行如下的設定:只有Xu\foobar賬號才具有訪問許可權。

我們修改了上面的程式碼,將驗證當前Windows賬號的程式碼替換成驗證檔案讀取許可權的程式碼。

class Program
{

    static void Main()
    {
        Console.WriteLine("Before impersonating: {0}", CanRead() ? "Allowed" : "Denied");
        using (GetWindowsIdentity("foobar", "password").Impersonate())
        {
            Console.WriteLine("Within Impersonation context: {0}", CanRead() ? "Allowed" : "Denied");
            new Thread(() => Console.WriteLine("Async thread: {0}", CanRead() ? "Allowed" : "Denied")).Start();
        }
        Console.WriteLine("Undo impersonation: {0}", CanRead() ? "Allowed" : "Denied");
        Console.Read();
        bool CanRead()
        {
            var userName = WindowsIdentity.GetCurrent().Name;
            try
            {
                File.ReadAllText(@"d:\test.txt");
                return true;
            }
            catch
            {
                return false;
            }
        }
    }
}

如下所示程式執行後的輸出結果,可以看出在檔案只有在針對XU\foobar的模擬上下文中才能被讀取。如果執行模擬WindowsIdentity的跨執行緒傳播,非同步執行緒也具有檔案讀取的許可權(如圖),否則在非同步執行緒中也無法讀取該檔案(感興趣的朋友可以自行測試一下)。

從執行上下文角度重新理解.NET(Core)的多執行緒程式設計[1]:基於呼叫鏈的”引數”傳遞
從執行上下文角度重新理解.NET(Core)的多執行緒程式設計[2]:同步上下文
從執行上下文角度重新理解.NET(Core)的多執行緒程式設計[3]:安全