目前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方法,對用戶名和密碼進行操做。mvc
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下來的,比較重點的就是裏面的app
epDisp.DispatchRuntime.MessageInspectors.Add(new ServiceInterpector());asp.net
這一行了,這一行比較特特,它在這裏引用了咱們另外一個類【ServiceInterpector】,兩個類的關係只有這一個地方。ServiceInterpector的代碼以下:ide
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中服務協定的名稱的方法。因此,也只能根據請求的地址來區分。spa
上面的類提供了兩種設定密碼的方式,一種是帶【地址】一種是不帶。客戶端調用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來使用它。配置以下:
<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節點加上 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就怎麼使用。