WCF安全之基于自定義聲明授權策略
背景
我們認為WCF可以使用自定的義的用戶名密碼方式限制對服務的訪問和加密,有可能存在這樣一種情況,比如一個協定中存在多個操作,即一個服務契約中包含多個操作契約,如果我們還希望在同一用戶訪問當前服務契約的時候,更進一步,可以讓我們做到對不同的用戶授予不同的操作契約的訪問,直白一點,比如有兩個用戶admin、admin2,admin可以訪問服務契約中的兩個操作契約,但是admin2只能訪問其中一個,本示例將實現這種需求,在以下的示例中,服務:IUserData 中包含三個操作契約,我們將對admin、admin2 這兩個用戶授予不同的操作契約的訪問權限,在客戶使用不同的用戶調用服務后,服務器將打印當前的請求服務的用戶、請求的資源、服務器對聲明的檢查、檢查結果等數據。
開始
首先我們建立一個自定義的基于服務授權訪問檢查的管理器,CustomServiceAuthorizationManager,首先看代碼:
- public class CustomServiceAuthorizationManager : ServiceAuthorizationManager
- {
- protected override bool CheckAccessCore(OperationContext operationContext)
- {
- string action = operationContext.RequestContext.RequestMessage.Headers.Action;
- Console.ForegroundColor = ConsoleColor.Red;
- Console.WriteLine("/--分割線--嘎嘎-------------------------------------------/");
- Console.ForegroundColor = ConsoleColor.White;
- Console.WriteLine("請求的資源,URI:{0}", action);
- foreach (ClaimSet cs in operationContext.ServiceSecurityContext.AuthorizationContext.ClaimSets)
- {
- if (cs.Issuer == ClaimSet.System)//如果此聲明是應用程序頒發的。
- {
- foreach (Claim claim in cs.FindClaims("net.tcp://UserDataService.IUserData/", Rights.PossessProperty))
- {
- Console.WriteLine("服務器聲明檢查,URI:{0}", action);
- if (claim.Resource.ToString() == action)
- {
- Console.WriteLine("通過,URI:{0}", action);
- return true;
- }
- }
- }
- }
- Console.WriteLine("不通過,URI:{0}", action);
- return false;
- }
1、在上面的代碼中,我們首先建立了一個繼承自 System.ServiceModel.ServiceAuthorizationManager,并重寫了其CheckAccessCore的檢查方法,或者有朋友會問,為什么不直接使用ServiceAuthorizationManager類呢?是的,沒錯,ServiceAuthorizationManager是提供自定義授權訪問檢查的類,但是,ServiceAuthorizationManager類本身并不對任何聲明進行評估,換句話說,此類不執行任何基于自定義聲明授權的檢查,在默認情況下,所有服務都是可以訪問的,顯示,這不符合我們本次示例的業務需求。
2、ServiceAuthorizationManager類共有三個可供重寫的方法,分別為:
(1)bool CheckAccess(OperationContext operationContext)
(2)bool CheckAccess(OperationContext operationContext, ref Message message)
(3)bool CheckAccessCore(OperationContext operationContext)
但是,實際上,前兩個方法都是通過調用bool CheckAccessCore(OperationContext operationContext) 來實現對自定義授權聲明的評估,所以,我們應該直接重寫CheckAccessCore而不是前兩個方法。
3、在重寫的CheckAccessCore方法內,首先通過當前操作的上下文對象獲得了當前客戶端所請求的資源定位符Action,接著,馬上調用自定義授權聲明頒發管理器,把當前用戶的授權策略聲明集添加到服務安全上下文,并檢查此授權策略聲明集是否為當前應用程序所頒發,如果是當前應用程序所頒發,則查找當前授權策略聲明中一個名為:“net.tcp://UserDataService.IUserData/”的聲明,最后,服務器再檢查如果用戶所請求的資源存在于此聲明中,剛授權通過,否則服務將拒絕客戶端所請求的資源。
自定義授權策略聲明集管理器
1、為了完成此次基于自定義授權策略聲明的服務器授權檢查, 我們需要構造自已的授權策略聲明集管理器,代碼如下:
- CustomAuthorizationPolicy
- public class CustomAuthorizationPolicy : IAuthorizationPolicy
- {
- string id = string.Empty;
- public CustomAuthorizationPolicy()
- {
- id = new Guid().ToString();//每個聲明集都是一個唯一的
- }
- /// <summary>
- /// 評估用戶是否符合基于此授權策略的聲明
- /// </summary>
- /// <param name="evaluationContext"></param>
- /// <param name="state"></param>
- /// <returns></returns>
- public bool Evaluate(EvaluationContext evaluationContext, ref object state)
- {
- bool flag = false;
- bool r_state = false;
- if (state == null) {tate = r_state;} else { r_state = Convert.ToBoolean(state);}
- if (!r_state)
- {
- IList<Claim> claims = new List<Claim>();//實體聲明集
- foreach (ClaimSet cs in evaluationContext.ClaimSets)
- {
- foreach (Claim claim in cs.FindClaims(ClaimTypes.Name, Rights.PossessProperty))
- {
- Console.WriteLine("用戶:{0}", claim.Resource);
- foreach (string str in GetOprationList(claim.Resource.ToString()))
- {
- claims.Add(new Claim("net.tcp://UserDataService.IUserData/", str, Rights.PossessProperty));
- Console.WriteLine("授權的資源:{0}", str);
- }
- }
- }
- evaluationContext.AddClaimSet(this, new DefaultClaimSet(Issuer, claims));r_state = true;flag = true;
- }
- else{flag = true;}
- return flag;
- }
- /// <summary>
- /// 賦予用戶聲明權限
- /// </summary>
- /// <param name="username"></param>
- /// <returns></returns>
- private static IEnumerable<string> GetOprationList(string username)
- {
- IList<string> lists = new List<string>();
- if (username == "admin")
- {
- lists.Add("net.tcp://UserService.IUserData/GetData");
- lists.Add("net.tcp://UserService.IUserData/GetUserExtensionXElement");
- }
- else if (username == "admin2")
- {
- lists.Add("net.tcp://UserService.IUserData/GetData");
- }
- return lists;
- }
- #region IAuthorizationComponent 成員/屬性實現
- public ClaimSet Issuer
- {
- get { return ClaimSet.System; }
- }
- public string Id
- {
- get { return id; }
- }
- #endregion
- }
2、首先,我們建立了一個類CustomAuthorizationPolicy繼承自IAuthorizationPolicy接口,IAuthorizationPolicy中定義了一組用于對用戶進行授權的規則,規則相當簡單,除了兩個屬性和一個授權規則檢查的方法,再沒有其它。
3、在類的一開始,我們給每個即將進行授權的客戶定義了一個聲明集,并給了一個唯一的標識(GUID),
4、實現了bool Evaluate(EvaluationContext evaluationContext, ref object state),在 Evaluate中,我們通過當前經過評估的授權策略的結果上下文對象獲取了一個ClaimSets對象,ClaimSets包含了一組經過評估的獲取與授權策略關聯的聲明集,實際上,包含了客戶端調用服務前添加的用戶名,這個用戶名是經過了上一章的自定義用戶名密碼訪問服務的驗證程序驗證后的用戶名,所以,我們可基于此用戶名對其權限進行分配,即給他一個可訪問服務操作的聲明。
5、添加聲明,在上面的代碼中,有一個方法:IEnumerable<string> GetOprationList(string username),在第4點中, bool Evaluate方法傳入一個用戶名,然后,GetOprationList方法將對這個用戶進行檢查并給其添加預先定義的聲明,當然,這個檢查你可以在持久化介質中做,比如數據庫,你大可將你的所有服務操作都做成基于角色的訪問的聲明,現在這里只是一個示例,所以沒有必要做得很靈活,當然,我也不推薦你那樣做,除非真有必要,我覺得效率上會有很大的問題。
6、不論這個用戶的權限如何,最后, Evaluate方法都會在將一個默認的聲明添加到當前當前經過評估的授權策略的結果上下文對象中,
這一句:evaluationContext.AddClaimSet(this, new DefaultClaimSet(Issuer, claims));
7、然后,再由前面定義的服務授權策略管理器對這個聲明和用戶所請求的資源進行檢查,以決定是否對該用戶所請求的資源授予其訪問權限。
配置授權策略管理器和授權聲明集管理器
1、在上面的代碼完成以后,我們還需要在服務器配置文件中進行相應的設置,好讓我們的代碼正常工作,配置比較簡單,如下:
- <behaviors>
- <serviceBehaviors>
- <serviceCredentials>
- <serviceCertificate findValue="192168168151service"
- x509FindType="FindBySubjectName"
- storeLocation="LocalMachine"
- storeName="My"/>
- <userNameAuthentication customUserNamePasswordValidatorType="UserDataServcie.CustomUserPassword,UserDataServcie" userNamePasswordValidationMode="Custom"/>
- </serviceCredentials>
- <serviceAuthorization serviceAuthorizationManagerType="UserDataServcie.CustomServiceAuthorizationManager,UserDataServcie">
- <authorizationPolicies>
- <add policyType="UserDataServcie.CustomAuthorizationPolicy,UserDataServcie"/>
- </authorizationPolicies>
- </serviceAuthorization>
- </behavior>
2、其中紅色部分是對 授權策略管理器和授權聲明集的配置,相當的簡單,現在讓我們來測試一下,客戶端代碼如下:
- UserDataClient client = new UserDataClient();
- //模擬admin用戶調用
- client.ClientCredentials.UserName.UserName = "admin";
- client.ClientCredentials.UserName.Password = "admin";
- Console.WriteLine("正在開始調用GetData方法");
- string msg = client.GetData(100);
- Console.WriteLine("調用結果:{0}", msg);
- Console.WriteLine("正在開始調用GetUserExtensionXElement方法");
- XElement xe = client.GetUserExtensionXElement(null);
- Console.WriteLine("調用成功,開始打印消息.");
- Console.ForegroundColor = ConsoleColor.Red;
- Console.WriteLine("==================================");
- Console.WriteLine(xe.Value);
- client.Close();
- //模擬admin2用戶調用
- client = new UserDataClient();
- Console.WriteLine("-----------------------------------------------------------------");
- client.ClientCredentials.UserName.UserName = "admin2";
- client.ClientCredentials.UserName.Password = "admin";
- Console.WriteLine("正在開始調用GetData方法");
- string msg2 = client.GetData(200);
- Console.WriteLine("調用結果:{0}", msg2);
- Console.WriteLine("正在開始調用GetUserExtensionXElement方法");
- XElement xe2 = client.GetUserExtensionXElement(null);
3、為了讓大家對服務器授權檢查是否成功有一個具體的認識,默認情況下,如果授權不通過,服務器將拋出一個SecurityAccessDeniedException異常,告訴客戶端,此調用未經服務器授權。我們也捕獲一下,并打印到控制臺:
- XElement xele = null;
- try
- {
- xele = base.Channel.GetUserExtensionXElement(xe);
- }
- catch (SecurityAccessDeniedException ex)
- {
- Console.WriteLine(ex.Message);
- }
結果
1、客戶端調用
2、服務器對客戶端進行聲明檢查
3、從上面的結果可以看出:
我們對admin用戶授予了對服務的兩個操作的聲明,所以兩個方法的服務調用都通過,
但是admin2用戶只授予了對服務的一個的聲明,所以當第其調用第二個操作的時候,授權未通過。
從這里可以看出,WCF對于安全配置提供了非常靈活的方式,讓我們可以隨心所欲的對服務訪問進行授權,但是,并不是所有的安全策略用上,服務才是安全的,還是那句話,有多大量,吃多大碗飯,別浪費了,呵呵~~
博文作者:梁規曉博客(http://www.cnblogs.com/viter/)!
【編輯推薦】