891
技術社區[雲棲]
[WCF權限控製]模擬(Impersonation)與委托(Delegation)[上篇]
由於服務操作是在寄宿進程中執行,在默認的情況下,服務操作是否具有足夠的權限訪問某個資源(比如文件)決定於執行寄宿進程Windows帳號的權限設置,而與作為客戶端的Windows帳號無關。在有多情況下,我們希望服務操作執行在基於客戶端的安全上下文中執行,以解決執行服務進行的帳號權限不足的問題。這就涉及到一個重要的話題——模擬(Impersonation)與委托(Delegation)[實例程序源代碼從這裏下載]
目錄:
一、從訪問令牌(Access Token)說起
二、再談WindowsIdentity
三、實例演示:通過身份模擬的方式讀取文件
當我們以某個Windows帳號的名義成功登錄Windows的時候,操作係統會創建一個訪問令牌(Access Token)。訪問令牌不僅僅包括登錄用戶身份相關信息,還包括該用戶具有的權限信息,比如ACL(Access Control List)。當我們開啟某個進程的時候,該訪問令牌會自動附加到該進程上,作為其安全上下文重要的組成部分。我們也可以將訪問令牌作為進程或者線程安全描述符的封裝。Windows下的訪問令牌主要具有如下兩種形式。
- 主令牌(Primary Token):每一個進程都具有一個唯一的主令牌,進行通過主令牌被開啟;
- 模擬令牌(Impersonation Token):在默認的情況下,當線程被開啟的時候,所在進程的主令牌會自動附加到當前線程上,作為線程的安全上下文。而線程可以運行在另一個非主令牌的訪問令牌下執行,而這個令牌被稱為模擬令牌。而指定線程的模擬令牌的過程被稱為模擬。
我們可以調用Win32函數,通過輸入Windows帳號、密碼、域名以及認證相關信息創建一個訪問令牌。LogonUser被調用的時候,會試圖進行基於本機的登陸操作。訪問令牌會在認證成功認證的情況下被創建並返回。LogonUser的定義如下,輸入參數依次代表的含義分別是用戶名、域名(可選參數)、密碼(明文)、登錄類型和登錄提供者。而創建的訪問令牌以輸出操作的形式返回。關於LogonUser函數的詳細說明,可以參考MSDN在線文檔。
1: BOOL LogonUser(
2: __in LPTSTR lpszUsername,
3: __in_opt LPTSTR lpszDomain,
4: __in LPTSTR lpszPassword,
5: __in DWORD dwLogonType,
6: __in DWORD dwLogonProvider,
7: __out PHANDLE phToken
8: );
當我們進行模擬而試圖創建一個模擬令牌的時候,我們需要使用到另外一個重要的Win32函數。顧名思義,該函數通過一個現有的令牌來“複製”一個新的令牌。DuplicateToken函數的定義如下麵的代碼片斷所示。
1: BOOL WINAPI DuplicateToken(
2: __in HANDLE ExistingTokenHandle,
3: __in SECURITY_IMPERSONATION_LEVEL ImpersonationLevel,
4: __out PHANDLE DuplicateTokenHandle
5: );
從上麵給出的DuplicateToken函數的定義我們可以看出:除了傳入現有的訪問令牌作為輸入參數之外,還具有一個表示模擬級別的ImpersonationLevel的參數。模擬級別是一個非常重要的概念,它表示被複製的模擬令牌可以被使用的程度和訪問。模擬等級通過如下所示的SECURITY_IMPERSONATION_LEVEL枚舉表示。
1: typedef enum _SECURITY_IMPERSONATION_LEVEL {
2: SecurityAnonymous,
3: SecurityIdentification,
4: SecurityImpersonation,
5: SecurityDelegation
6: } SECURITY_IMPERSONATION_LEVEL, *PSECURITY_IMPERSONATION_LEVEL;
四個枚舉項,SecurityAnonymous、SecurityIdentification、SecurityImpersonation和SecurityDelegation分別代表如下四種模擬的等級。
- 匿名(Anonymous):無法獲取有關客戶端的標識信息,且無法模擬客戶端;
- 識別(Identification):可以獲取有關客戶端的信息(如安全標識符和特權),但是無法模擬客戶端;
- 模擬(Impersonation):可以在本地模擬客戶端的安全上下文。,但無法在遠程係統上模擬客戶端;
- 委托(Delegation):可以在本地和遠程係統上模擬客戶端的安全上下文。
對於模擬級別,對於托管代碼的應用編程接口中也具有一個匹配的枚舉TokenImpersonationLevel。如下麵的代碼片斷所示,除了上述的四個對應於具體模擬級別的枚舉項之外,TokenImpersonationLevel還有一個額外的枚舉值None,表示具體的模擬級別上為指定。
1: public enum TokenImpersonationLevel
2: {
3: None,
4: Anonymous,
5: Identification,
6: Impersonation,
7: Delegation
8: }
關於模擬在托管代碼的實現,我們不得不提我們之前已經介紹過的類型WindowsIdentity。從某種意義上講,一個WindowsIdentity對象可以看成是對一個訪問令牌的封裝。WindowsIdentity直接為我們提供了模擬的功能。從下麵給出的WindowsIdentity部分成員定義可以看到,它具有一個IntPtr類型的隻讀屬性,實際上代表的就是我們上麵介紹的訪問令牌。而我們可以通過直接指定這個訪問令牌創建一個WindowsIdentity對象。ImpersonationLevel表示訪問令牌的模擬級別。
1: public class WindowsIdentity : IIdentity,...
2: {
3: //其他成員
4: public WindowsIdentity(IntPtr userToken);
5: public virtual IntPtr Token { get; }
6: public TokenImpersonationLevel ImpersonationLevel { get; }
7:
8:
9: public virtual WindowsImpersonationContext Impersonate();
10: public static WindowsImpersonationContext Impersonate(IntPtr userToken);
11: }
對應一個現有的WindowsIdentity對象,隻要其訪問令牌的模擬級別為Impersonation或者Delegation,我們就能調用Impersonate方法模擬這個身份。此外WindowsIdentity還提供了靜態的Impersonate是我們可以直接根絕一個訪問令牌實施身份模式。一旦Impersonate方法被調用,。Impersonate方法返回的是一個WindowsImpersonationContext對象。如果需要將安全上下文恢複到模擬之前的狀態,可以調用WindowsImpersonationContext的Undo方法。而WindowsImpersonationContext實現了IDisposable接口,Undo方法實際上也會在Dispose方法中被調用。WindowsImpersonationContext定義如下。
1: public class WindowsImpersonationContext : IDisposable
2: {
3: public void Dispose();
4: public void Undo();
5: }
一般來說,隻有某些特殊的操作(比如訪問一個受權限控製的文件)才需要在被模擬的身份下執行。當這些操作執行完畢或者在執行過程中拋出異常,我們都需要恢複線程安全上下文到被模式之前的狀態。所以正確的模擬編程應該采用如下的方式。
1: WindowsImpersonationContext impersonationContext = identity.Impersonate();
2: try
3: {
4: //在模擬身份下執行的操作
5: }
6: catch (Exception ex)
7: {
8: //異常處理
9: }
10: finally
11: {
12: impersonationContext.Undo();
13: }
或者
1: using (WindowsImpersonationContext impersonationContext = identity.Impersonate())
2: {
3: //在模擬身份下執行的操作
4: }
有一點需要注意的是:。
為了讓讀者對身份模式的作用和實現具有一個深刻的認識,我們來演示一個簡單的實例。在這個實例中,我們將通過ACL設置一個文件的讀取權限,然後演示針對不同Windows帳號進行模擬的情況下,是否能夠正常讀取該文件。
我們需要創建了兩個本機的Windows帳戶Foo和Bar(密碼為Password)。然後你在某個公共的目錄下(比如D:\盤)創建一個簡單的文本(比如impersonationTest.txt)。然後。
然後我們創建一個簡單的控製台應用作為演示程序。在默認創建的Program類中,定義如下一個CreateWindowsIdentity靜態方法。該方法通過輸入用戶名、密碼和模擬級別創建相應的WindowsIdentity。在CreateWindowsIdentity方法內部實際上是直接調用了上麵介紹的兩個Win32函數LogonUser和DuplicateToken。
1: class Program
2: {
3: //其他成員
4: public const int LOGON32_PROVIDER_DEFAULT = 0;
5: public const int LOGON32_LOGON_INTERACTIVE = 2;
6:
7: [DllImport("advapi32.dll", SetLastError = true)]
8: public static extern bool LogonUser(string userName, string domainName, string password, int logonType, int loggonProvider, ref IntPtr token);
9:
10: [DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
11: public extern static bool DuplicateToken(IntPtr existingToken, int imperonationLevel, ref IntPtr newToken);
12:
13: public static WindowsIdentity CreateWindowsIdentity(string userName, string password, TokenImpersonationLevel tokenImpersonationLevel)
14: {
15: IntPtr token = IntPtr.Zero;
16: IntPtr duplicateToken = IntPtr.Zero;
17: if (LogonUser(userName, string.Empty, password, LOGON32_LOGON_INTERACTIVE, LOGON32_PROVIDER_DEFAULT, ref token))
18: {
19: int impersonationLevel;
20: switch (tokenImpersonationLevel)
21: {
22: case TokenImpersonationLevel.Anonymous:
23: { impersonationLevel = 0; break; }
24: case TokenImpersonationLevel.Impersonation:
25: { impersonationLevel = 2; break; }
26: case TokenImpersonationLevel.Delegation:
27: { impersonationLevel = 3; break; }
28: default:
29: { impersonationLevel = 1; break; }
30: }
31: if (DuplicateToken(token, impersonationLevel, ref duplicateToken))
32: {
33: return new WindowsIdentity(duplicateToken);
34: }
35: else
36: {
37: throw new InvalidOperationException(string.Format("創建模擬令牌失敗 (錯誤代碼: {0}) ", Marshal.GetLastWin32Error()));
38: }
39: }
40: else
41: {
42: throw new InvalidOperationException(string.Format("用戶登錄失敗 (錯誤代碼: {0}) ", Marshal.GetLastWin32Error()));
43: }
44: }
45: }
然後我們定義如下一個靜態的ReadFile方法。在這個方法中,我們根據傳入的用戶名和密碼調用上述的CreateWindowsIdentity方法創建相應的WindowsIdentity。然後模擬該用戶進行文件的讀取。在成功讀取和拋出異常的情況下分別輸出相應的指示性文字。在Main方法中,分別傳入傳入賬號Foo和Bar以及相應的密碼對該方法進行調用。從輸出的結果可以看出,在模擬帳號Foo時,文件被成功讀取,而在模擬帳號Bar的時候卻失敗了。這和測試文件的ACL設置時一致的。
1: public static void ReadFile(string userName, string password)
2: {
3: try
4: {
5: WindowsIdentity identity = CreateWindowsIdentity(userName, password, TokenImpersonationLevel.Impersonation);
6: using (WindowsImpersonationContext context = identity.Impersonate())
7: {
8: Console.WriteLine("當前用為:{0}", WindowsIdentity.GetCurrent().Name);
9: File.ReadAllText("D:\\impersonationTest.txt");
10: Console.WriteLine("成功讀取文件內容...");
11: }
12: }
13: catch
14: {
15: Console.WriteLine("讀取文件失敗...");
16: }
17: }
18: static void Main(string[] args)
19: {
20: ReadFile("Foo", "Password");
21: ReadFile("Bar", "Password");
22: }
輸出結果(Jinnan-PC為我本機的機器名):
1: 當前用戶為:Jinnan-PC\Foo
2: 成功讀取文件內容...
3: 當前用戶為:Jinnan-PC\Bar
4: 讀取文件失敗...
微信公眾賬號:大內老A
微博:www.weibo.com/artech
如果你想及時得到個人撰寫文章以及著作的消息推送,或者想看看個人推薦的技術資料,可以掃描左邊二維碼(或者長按識別二維碼)關注個人公眾號(原來公眾帳號蔣金楠的自媒體將會停用)。
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁麵明顯位置給出原文連接,否則保留追究法律責任的權利。
最後更新:2017-10-26 16:34:06