目前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就怎么使用。