在前兩篇文章(《基於調用鏈的”參數”傳遞》和《同步上下文》)中,我們先后介紹了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]:安全上下文