自定义实现wcf的用户名密码验证
目前wcf分为【传输层安全】【消息层安全】两种,本身也自带的用户名密码验证的功能,但是ms为了防止用户名密码明文在网络上传输,所以,强制要求一旦使用【用户名密码】校验功能,则必须使用证书,按照常理讲,这是对的,但是我们的环境特殊。由于处于各级的路由器之下,加上ssl的性能问题,我们通过统一的网关进行ssl处理,也就是说,客户端到路由之间走的是https,而路由到我们的服务器之间走的则是http,这样使得证书集中管理,性能也有所提升。但是,却用不了wcf自己的【用户名密码校验】功能。
经过在网上找寻资料以及参照各种代码,最终决定使用【behaviorExtensions】来解决这个问题,【behaviorExtensions】的好处是可以让我们实现与asp.net mvc类似的AOP功能,即面向切面,我们可以为wcf创建【Interpector】(mvc中的【filter】们)来对一个接口的方法被调用前后进行处理。我们的设计是在接口被调用前从【message】的【header】中取得我们事先在客户端写入的【用户名】【密码】,然后进行校验,如果通过,则继续执行,否则报错直接终止请求进程。
实现的代码网上有很多,这里我只为客户端封装了一个dll进行使用,而服务端我则是写死在代码中的,感觉没有必要。
服务器一共两个类,行为类【ServiceBehavior】,检测类【ServiceInterpector】,我们这样理解,【ServiceBehavior】是用来被wcf识别并且配置到具体的服务协定中的,因为它是一个【Behavior】,而【ServiceInterpector】则是在【ServiceBehavior】中被调用,执行Validate方法,对用户名和密码进行操作。
public class ServiceBehavior : BehaviorExtensionElement, IServiceBehavior { public override Type BehaviorType { get { return typeof(ServiceBehavior); } } protected override object CreateBehavior() { return new ServiceBehavior(); } #region IServiceBehavior Members public void AddBindingParameters(ServiceDescription serviceDescription, System.ServiceModel.ServiceHostBase serviceHostBase, System.Collections.ObjectModel.Collection<ServiceEndpoint> endpoints, System.ServiceModel.Channels.BindingParameterCollection bindingParameters) { } public void ApplyDispatchBehavior(ServiceDescription serviceDescription, System.ServiceModel.ServiceHostBase serviceHostBase) { foreach (ChannelDispatcher chDisp in serviceHostBase.ChannelDispatchers) { foreach (EndpointDispatcher epDisp in chDisp.Endpoints) { epDisp.DispatchRuntime.MessageInspectors.Add(new ServiceInterpector()); } } } public void Validate(ServiceDescription serviceDescription, System.ServiceModel.ServiceHostBase serviceHostBase) { } #endregion }
上面这个【ServiceBehavior】完全通用,我也是在【网上】直接down下来的,比较重点的就是里面的
epDisp.DispatchRuntime.MessageInspectors.Add(new ServiceInterpector());
这一行了,这一行比较特特,它在这里引用了我们另一个类【ServiceInterpector】,两个类的关系只有这一个地方。ServiceInterpector的代码如下:
public class ServiceInterpector : IDispatchMessageInspector { #region IDispatchMessageInspector Members public object AfterReceiveRequest(ref System.ServiceModel.Channels.Message request, System.ServiceModel.IClientChannel channel, System.ServiceModel.InstanceContext instanceContext) { Console.WriteLine(request); var username = GetHeaderValue("OperationUserName"); var password = GetHeaderValue("OperationPassword"); var validcode = GetHeaderValue("OperationValidCode"); if(!string.IsNullOrEmpty(request.Headers.Action)) { if (!B_UserValidate.Validate(username, password)) { throw new MsgException("非法的用户名与密码!"); } } return null;//if success return null. } public void BeforeSendReply(ref System.ServiceModel.Channels.Message reply, object correlationState) { } private string GetHeaderValue(string name, string ns = "http://tempuri.org") { var headers = OperationContext.Current.IncomingMessageHeaders; var index = headers.FindHeader(name, ns); if (index > -1) return headers.GetHeader<string>(index); else return null; } #endregion }
注意上面【B_UserValidate.Validate(username, password)】这句代码,是用来判断用户名和密码是否合法的,至于【OperationValidCode】这个东西,取出来是为以后使用的,这个地方并没有起到任何作用。这就是服务端,其实非常简单,因为一个服务做一个就可以了,也不存在不同的验证体系的问题。
但是,客户端就悲剧了,因为客户端有可能同时引用多个wcf,而这多个wcf都使用我上面这种用户名密码验证,而客户端的开发人员又不想在开发的时候每次调用前都带上【用户名】和【密码】,一次设定多次使用是必须的要求,所以,只能使用字典来实现。这里我们用了一个类,如下:
public static class UserModelStatic { private static Dictionary<string, string[]> dicUserName = new Dictionary<string,string[]>(); /// <summary> /// 设置用户名及密码,在用户调用对应的WCF地址时,将会使用此用户名密码 /// 如果重复设置,最新的将会覆盖旧的 /// </summary> /// <param name="Address">wcf的地址,例如: http://www.test.com/myservice.svc</param> /// <param name="username">用户名</param> /// <param name="passsword">密码</param> public static void SetUsernamePassword(string Address,string username,string passsword) { if(Address==null || username==null || passsword==null) { throw new Exception("用户名或密码或wcf地址为空"); } Address = Address.ToUpper(); string[] up = new string[2] { username,passsword }; if(!dicUserName.ContainsKey(Address)) { dicUserName.Add(Address, up); } else { dicUserName[Address] = up; } } /// <summary> /// 设置用户名及密码,如果用户调用某一wcf但是没有设置此wcf地址设定的用户名密码,则会默认使用此处设置的用户名和密码 /// 如果重复设置,最新的将会覆盖旧的 /// </summary> /// <param name="username">用户名</param> /// <param name="passsword">密码</param> public static void SetUsernamePassword(string username,string password) { UserNamePasswordDefault = new string[2] { username, password }; } /// <summary> /// 获取用户名和密码 /// </summary> /// <returns>{"用户名","密码"}</returns> public static string[] GetUserPassword() { return UserNamePasswordDefault; } /// <summary> /// 获取用户名和密码 /// </summary> /// <returns>{"用户名","密码"}</returns> /// <param name="Address">wcf接口的地址,例如: http://www.test.com/myservice.svc </param> public static string[] GetUserPassword(string Address) { Address = Address.ToUpper(); if(!dicUserName.ContainsKey(Address)) { return null; } else { return dicUserName[Address]; } } private static string[] UserNamePasswordDefault = null; }
这个类用于全局保存【用户名】和【密码】,因为我所面向的是直接在vs里面生成服务引用代码的同学们,但是,我却没能在客户端的【Interpector】找到取得app.config中服务协定的名称的方法。所以,也只能根据请求的地址来区分。
上面的类提供了两种设定密码的方式,一种是带【地址】一种是不带。客户端调用wcf时会先根本调用的地方去取用户名和密码,如果没取到,则会使用那个唯一一个不带【地址】的公共【用户名】【密码】,如果还是取不到,则进抛出异常。抛出异常的目的是为了在测试的时候发现问题,而且强制一旦配置了一个引用使用这种用户名密码验证的行为就被对用户名和密码进行设定——哪怕是错的。
客户端一共有三个类,除了上面这种,以及与服务端一样的【ServiceBehavior】类(代码在上文中),就只有一个【Interpector】不同,这个【Interpector】处理了客户端所有的关于【用户名密码】登陆的逻辑,代码如下:
public class UserNameValidateClientInterpector : IClientMessageInspector { public void AfterReceiveReply(ref Message reply, object correlationState) { } public object BeforeSendRequest(ref Message request, IClientChannel channel) { string wcfAddress = channel.Via.ToString(); string[] up = UserModelStatic.GetUserPassword(wcfAddress); if (up == null) { up = UserModelStatic.GetUserPassword(); } if (up == null) { throw new Exception("您的验证信息尚未填写,请填写后市调用WCF"); } var userNameHeader = MessageHeader.CreateHeader("OperationUserName", "http://tempuri.org", up[0], false, ""); var passwordHeader = MessageHeader.CreateHeader("OperationPassword", "http://tempuri.org", up[1], false, ""); request.Headers.Add(userNameHeader); request.Headers.Add(passwordHeader); Console.WriteLine(request); return null; } }
这样,一切就都OK了,将客户端的三个类封装在一个单独的DLL中,将服务端的两类写在服务端的项目中,重点在下面,我们需要进行配置了。
服务端的配置
服务的配置有这样几个目的
首先,你要让wcf能找到你这个behavior,配置如下:
<system.serviceModel> <extensions> <behaviorExtensions> <add name="UserNameValidateServiceBehavior" type="TestWcf.ServiceBehavior, TestWcf, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" /> </behaviorExtensions> </extensions>
虽然写在同一个项目中,但是它却无法自己找到,这个很难过,只能配置了,而且还有加上type来配置,之所以放在wcf项目中也是因为这个type。type是一个用,隔开的字符串,一共五段,第一段是你这人Behavior类的【强命名】(强命名就是 命名空间+类名),第二段是你这个Behavior所在的程序集名称,一般就是你的项目名,也就是你的项目生成的dll或者exe的名称(注意不带.dll和.exe),第三段则是版本号,这个版本号可以在你的项目的Properties里面的Assembly.cs里面找到,截图如下:
再信下就是程序集签名了,我们目前只有在特殊的情况下才对程序集进行签名来控制版本,而这个项目我们没有签,所以直接写null就可以了,想看自己有没有签,只需要在项目的属性里面找到【签名】这一标签页
这个如果选中了,就说明签名了,这时候你会有一串字符串,填在上面最后一段即可。
就这样,我们就完成了服务端的第一个配置。
其次,你要让你的这个程序集成为一个真的Behavior
这句话的意思是,我们在第一步中的操作只是引用了这个类,但是却没有给这个类应有的身份,所以,我们需要一个Behavior来使用它。配置如下:
<behaviors> <serviceBehaviors>
<behavior name="ServiceInterpectorBehavior"> <dataContractSerializer maxItemsInObjectGraph="2147483646" /> <serviceMetadata httpGetEnabled="true" /> <serviceDebug includeExceptionDetailInFaults="true" /> <serviceThrottling maxConcurrentCalls="20" maxConcurrentSessions="20" maxConcurrentInstances="20" /> <UserNameValidateServiceBehavior /> </behavior> </serviceBehaviors> </behaviors>
有人问我,为何这么多,其实只有UserNameValidateServiceBehavior这一个有用,所以,让我们来精简一下:
<behaviors> <serviceBehaviors> <behavior name="ServiceInterpectorBehavior"> <UserNameValidateServiceBehavior /> </behavior> </serviceBehaviors> </behaviors>
来解释一下,上面配置的含义,<behavior name=,这个东西,这个name是自己给这个新的behavior起的名字,在下面的地方需要使用。<UserNameValidateServiceBehavior /> 这个东西是重要,原因是它就是我们上面引用的我们的【ServiceBehavior】类,回到第一步配置的图,里面可以看见我们引用的时候给那个引用的节点起了个名字,就叫“UserNameValidateServiceBehavior ”,所以,在这里直接以节点的形势将它加给behavior就可以了。
第三步,就是配置服务端的service
在你需要使用【用户名密码验证】的服务的service节点加上 behaviorConfiguration="ServiceInterpectorBehavior",就可以了。
客户端的配置
客户端和配置和服务端一样,只是第三步配置的不是service,而是client节点。
另外一定要确保dll被客户端引用并且属性里面设置复制到本地,然后它的type设定的时候,直接在dll生成的项目里面看就可以了。
客户端的使用
客户端在使用的时候,问题就不大了,在你调用需要【用户名】【密码】验证的服务之前,请配置你的【UserModelStatic】中的用户名和密码。比如【WcfClientInterpector】是我的dll。那么我在Program.cs里面是这样配置的
WcfClientInterpector.UserModelStatic.SetUsernamePassword(“http://localhost:3868/TestWcf.svc”, "ensleep", "password");
这样,你后面凡是调用【http://localhost:3868/TestWcf.svc】这个地址的wcf服务时,都会带上你设置的【用户名】和【密码】,假如你的服务比如多,但是【用户名】【密码】都一样,你可以这样设置:
WcfClientInterpector.UserModelStatic.SetUsernamePassword( "ensleep", "password");
这种情况下,如果系统没找到你请求的wcf服务的地址对应的用户名和密码,则会使用你设定的这处公用的【用户名和密码】。
至于其它的,已经ok了,以前该怎么使用wcf就怎么使用。