背景
我们认为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/)!
【编辑推荐】