WCF身份驗證一般常見的方式有:自定義用戶名及密碼驗證、X509證書驗證、ASP.NET成員資格(membership)驗證、SOAP Header驗證、Windows集成驗證、WCF身份驗證服務(AuthenticationService),這些驗證方式其實網上都有相關的介紹文章,我這里算是一個總結吧,順便對於一些注意細節進行說明,以便大家能更好的掌握這些知識。
第一種:自定義用戶名及密碼驗證(需要借助X509證書)
由於該驗證需要借助於X509證書,所以我們需要先創建一個證書,可以利用MS自帶的makecert.exe程序來制作測試用證書,使用步驟:請依次打開開始->Microsoft Visual Studio 2010(VS菜單,版本不同,名稱有所不同)->Visual Studio Tools->Visual Studio 命令提示,然后執行以下命令:
makecert -r -pe -n "CN=ZwjCert" -ss TrustedPeople -sr LocalMachine -sky exchange
上述命令中除了我標粗的部份可改成你實際的請求外(為證書名稱),其余的均可以保持不變,命令的意思是:創建一個名為ZwjCert的證書將將其加入到本地計算機的受信任人區域中。
如果需要查看該證書,那么可以通過MMC控制台查詢證書,具體操作步驟如下:
運行->MMC,第一次打開Windows沒有給我們准備好直接的管理證書的入口,需要自行添加,添加方法如下:
1. 在控制台菜單,文件→添加/刪除管理單元→添加按鈕→選”證書”→添加→選”我的用戶賬戶”→關閉→確定
2. 在控制台菜單,文件→添加/刪除管理單元→添加按鈕→選”證書”→添加→選”計算機賬戶”→關閉→確定
這樣MMC中左邊就有菜單了,然后依次展開:證書(本地計算機)->受信任人->證書,最后就可以在右邊的證書列表中看到自己的證書了,如下圖示:
證書創建好,我們就可以開始編碼了,本文主要講的就是WCF,所以我們首先定義一個WCF服務契約及服務實現類(后面的各種驗證均采用該WCF服務),我這里直接采用默認的代碼,如下:
namespace WcfAuthentications { [ServiceContract] public interface IService1 { [OperationContract] string GetData(int value); [OperationContract] CompositeType GetDataUsingDataContract(CompositeType composite); } [DataContract] public class CompositeType { bool boolValue = true; string stringValue = "Hello "; [DataMember] public bool BoolValue { get { return boolValue; } set { boolValue = value; } } [DataMember] public string StringValue { get { return stringValue; } set { stringValue = value; } } } } namespace WcfAuthentications { public class Service1 : IService1 { public string GetData(int value) { return string.Format("You entered: {0}", value); } public CompositeType GetDataUsingDataContract(CompositeType composite) { if (composite == null) { throw new ArgumentNullException("composite"); } if (composite.BoolValue) { composite.StringValue += "Suffix"; } return composite; } } }
要實現用戶名及密碼驗證,就需要定義一個繼承自UserNamePasswordValidator的用戶名及密碼驗證器類CustomUserNameValidator,代碼如下:
namespace WcfAuthentications { public class CustomUserNameValidator : UserNamePasswordValidator { public override void Validate(string userName, string password) { if (null == userName || null == password) { throw new ArgumentNullException(); } if (userName != "admin" && password != "wcf.admin") //這里可依實際情況下實現用戶名及密碼判斷 { throw new System.IdentityModel.Tokens.SecurityTokenException("Unknown Username or Password"); } } } }
代碼很簡單,只是重寫其Validate方法,下面就是將創建WCF宿主,我這里采用控制台程序
代碼部份:
namespace WcfHost { class Program { static void Main(string[] args) { using (var host = new ServiceHost(typeof(Service1))) { host.Opened += delegate { Console.WriteLine("Service1 Host已開啟!"); }; host.Open(); Console.ReadKey(); } } } }
APP.CONFIG部份(這是重點,可以使用WCF配置工具來進行可視化操作配置,參見:http://www.cnblogs.com/Moosdau/archive/2011/04/17/2019002.html):
<system.serviceModel> <bindings> <wsHttpBinding> <binding name="Service1Binding"> <security mode="Message"> <message clientCredentialType="UserName" /> </security> </binding> </wsHttpBinding> </bindings> <services> <service behaviorConfiguration="Service1Behavior" name="WcfAuthentications.Service1"> <endpoint address="" binding="wsHttpBinding" bindingConfiguration="Service1Binding" contract="WcfAuthentications.IService1"> <identity> <dns value="ZwjCert" /> </identity> </endpoint> <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange" /> <host> <baseAddresses> <add baseAddress="http://localhost:8732/WcfAuthentications/Service1/" /> </baseAddresses> </host> </service> </services> <behaviors> <serviceBehaviors> <behavior name="Service1Behavior"> <serviceMetadata httpGetEnabled="true" /> <serviceDebug includeExceptionDetailInFaults="false" /> <serviceCredentials> <serviceCertificate findValue="ZwjCert" x509FindType="FindBySubjectName" storeLocation="LocalMachine" storeName="TrustedPeople" /> <userNameAuthentication userNamePasswordValidationMode="Custom" customUserNamePasswordValidatorType="WcfAuthentications.CustomUserNameValidator,WcfAuthentications" /> </serviceCredentials> </behavior> </serviceBehaviors> </behaviors> </system.serviceModel>
這里面有幾個需要注意的點:
1.<dns value="ZwjCert" />與<serviceCertificate findValue="ZwjCert" ..>中的value必需都為證書的名稱,即:ZwjCert;
2.Binding節點中需配置security節點,message子節點中的clientCredentialType必需設為:UserName;
3.serviceBehavior節點中,需配置serviceCredentials子節點,其中serviceCertificate 中各屬性均需與證書相匹配,userNameAuthentication的userNamePasswordValidationMode必需為Custom,customUserNamePasswordValidatorType為上面自定義的用戶名及密碼驗證器類的類型及其程序集
最后就是在客戶端使用了,先引用服務,然后看下App.Config,並進行適當的修改,如下:
<system.serviceModel> <bindings> <wsHttpBinding> <binding name="WSHttpBinding_IService1" > <security mode="Message"> <transport clientCredentialType="Windows" proxyCredentialType="None" realm="" /> <message clientCredentialType="UserName" negotiateServiceCredential="true" algorithmSuite="Default" /> </security> </binding> </wsHttpBinding> </bindings> <client> <endpoint address="http://localhost:8732/WcfAuthentications/Service1/" binding="wsHttpBinding" bindingConfiguration="WSHttpBinding_IService1" contract="ServiceReference1.IService1" name="WSHttpBinding_IService1"> <identity> <dns value="ZwjCert" /> </identity> </endpoint> </client> </system.serviceModel>
為了突出重點,我這里對Binding節點進行了精簡,去掉了許多的屬性配置,僅保留重要的部份,如:security節點,修改其endpoint下面的identity中<dns value="ZwjCert" />,這里的value與服務中所說的相同節點相同,就是證書名稱,如果不相同,那么就會報錯,具體的錯誤消息大家可以自行試下,我這里限於篇幅內容就不貼出來了。
客戶端使用服務代碼如下:
namespace WCFClient { class Program { static void Main(string[] args) { using (var proxy = new ServiceReference1.Service1Client()) { proxy.ClientCredentials.UserName.UserName = "admin"; proxy.ClientCredentials.UserName.Password = "wcf.admin"; string result = proxy.GetData(1); Console.WriteLine(result); var compositeObj = proxy.GetDataUsingDataContract(new CompositeType() { BoolValue = true, StringValue = "test" }); Console.WriteLine(SerializerToJson(compositeObj)); } Console.ReadKey(); } /// <summary> /// 序列化成JSON字符串 /// </summary> static string SerializerToJson<T>(T obj) where T:class { var serializer = new DataContractJsonSerializer(typeof(T)); var stream = new MemoryStream(); serializer.WriteObject(stream,obj); byte[] dataBytes = new byte[stream.Length]; stream.Position = 0; stream.Read(dataBytes, 0, (int)stream.Length); string dataString = Encoding.UTF8.GetString(dataBytes); return dataString; } } }
運行結果如下圖示:
如果不傳入用戶名及密碼或傳入不正確的用戶名及密碼,均會報錯:
第二種:X509證書驗證
首先創建一個證書,我這里就用上面創建的一個證書:ZwjCert;由於服務器端及客戶端均需要用到該證書,所以需要導出證書,在客戶端的電腦上導入該證書,以便WCF可進行驗證。
WCF服務契約及服務實現類與第一種方法相同,不再重貼代碼。
WCF服務器配置如下:
<system.serviceModel> <bindings> <wsHttpBinding> <binding name="Service1Binding"> <security mode="Message"> <message clientCredentialType="Certificate" /> </security> </binding> </wsHttpBinding> </bindings> <services> <service behaviorConfiguration="Service1Behavior" name="WcfAuthentications.Service1"> <endpoint address="" binding="wsHttpBinding" bindingConfiguration="Service1Binding" contract="WcfAuthentications.IService1"> </endpoint> <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange" /> <host> <baseAddresses> <add baseAddress="http://127.0.0.1:8732/WcfAuthentications/Service1/" /> </baseAddresses> </host> </service> </services> <behaviors> <serviceBehaviors> <behavior name="Service1Behavior"> <serviceMetadata httpGetEnabled="true" /> <serviceDebug includeExceptionDetailInFaults="false" /> <serviceCredentials> <serviceCertificate findValue="ZwjCert" x509FindType="FindBySubjectName" storeLocation="LocalMachine" storeName="TrustedPeople" /> <clientCertificate> <authentication certificateValidationMode="None"/> </clientCertificate> </serviceCredentials> </behavior> </serviceBehaviors> </behaviors> </system.serviceModel>
這里需注意如下幾點:
1.<message clientCredentialType="Certificate" />clientCredentialType設為:Certificate;
2.需配置serviceCredentials節點,其中serviceCertificate 中各屬性均需與證書相匹配,clientCertificate里面我將authentication.certificateValidationMode="None",不設置采用默認值其實也可以;
客戶端引用服務,自動生成如下配置信息:
<system.serviceModel> <bindings> <wsHttpBinding> <binding name="WSHttpBinding_IService1"> <security mode="Message"> <transport clientCredentialType="Windows" proxyCredentialType="None" realm="" /> <message clientCredentialType="Certificate" negotiateServiceCredential="true" algorithmSuite="Default" /> </security> </binding> </wsHttpBinding> </bindings> <client> <endpoint address="http://127.0.0.1:8732/WcfAuthentications/Service1/" binding="wsHttpBinding" bindingConfiguration="WSHttpBinding_IService1" contract="ServiceReference1.IService1" name="WSHttpBinding_IService1" behaviorConfiguration="Service1Nehavior"> <identity> <certificate encodedValue="AwAAAAEAAAAUAAAAkk2avjNCItzUlS2+Xj66ZA2HBZYgAAAAAQAAAOwBAAAwggHoMIIBVaADAgECAhAIAOzFvLxLuUhHJRwHUUh9MAkGBSsOAwIdBQAwEjEQMA4GA1UEAxMHWndqQ2VydDAeFw0xNTEyMDUwMjUyMTRaFw0zOTEyMzEyMzU5NTlaMBIxEDAOBgNVBAMTB1p3akNlcnQwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBALfGfsiYpIVKu3gPJl790L13+CZWt6doePZHmcjMl+xPQKIR2fDvsCq9ZxzapDgiG4T3mgcVKUv55DBiuHcpXDvXt28m49AjdKwp924bOGKPM56eweKDCzYfLxy5SxaZfA9qjUhnPq3kGu1lfWjXbsp1rKI1UhKJg5b2j0V7AOC3AgMBAAGjRzBFMEMGA1UdAQQ8MDqAEH/MEXV8FHNLtxvllQ5SMbihFDASMRAwDgYDVQQDEwdad2pDZXJ0ghAIAOzFvLxLuUhHJRwHUUh9MAkGBSsOAwIdBQADgYEAdBtBNTK/Aj3woH2ts6FIU3nh7FB2tKQ9L3k6QVL+kCR9mHuqWtYFJTBKxzESN2t0If6muiktcO+C8iNwYpJpPzLAOMFMrTQhkO82gcdr9brQzMWPTraK1IS+GGH8QBIOTLx9zfV/iCIXxRub+Sq9dmRSQjKDeLeHWoE5I6FkQJg=" /> </identity> </endpoint> </client> <behaviors> <endpointBehaviors> <behavior name="Service1Nehavior"> <clientCredentials> <clientCertificate findValue="ZwjCert" x509FindType="FindBySubjectName" storeLocation="LocalMachine" storeName="TrustedPeople" /> </clientCredentials> </behavior> </endpointBehaviors> </behaviors> </system.serviceModel>
可以看出endpoint節點下的identity.certificate的encodedValue包含了加密的數據,另外需要手動增加clientCertificate配置信息,該信息表示證書在本地電腦存放的位置,當然也可以通過代碼來動態指定,如:proxy.ClientCredentials.ClientCertificate.SetCertificate("ZwjCert", StoreLocation.LocalMachine, StoreName.My);
客戶端使用服務代碼如下:
static void Main(string[] args) { using (var proxy = new ServiceReference1.Service1Client()) { //proxy.ClientCredentials.ClientCertificate.SetCertificate("ZwjCert", StoreLocation.LocalMachine, StoreName.My); //直接動態指定證書存儲位置 string result = proxy.GetData(1); Console.WriteLine(result); var compositeObj = proxy.GetDataUsingDataContract(new CompositeType() { BoolValue = true, StringValue = "test" }); Console.WriteLine(SerializerToJson(compositeObj)); } Console.ReadKey(); }
網上還有另類的針對X509證書驗證,主要是采用了自定義的證書驗證器類,有興趣的可以參見這篇文章:http://www.cnblogs.com/ejiyuan/archive/2010/05/31/1748363.html
第三種:ASP.NET成員資格(membership)驗證
由於該驗證需要借助於X509證書,所以仍然需要創建一個證書(方法如第一種中創建證書方法相同):ZwjCert;
由於該種驗證方法是基於ASP.NET的membership,所以需要創建相應的數據庫及創建賬號,創建數據庫,請通過運行aspnet_regsql.exe向導來創建數據庫及其相關的表,通過打開ASP.NET 網站管理工具(是一個自帶的管理網站),並在上面創建角色及用戶,用於后續的驗證;
這里特別說明一下,若采用VS2013,VS上是沒有自帶的GUI按鈕來啟動該管理工具網站,需要通過如下命令來動態編譯該網站:
cd C:\Program Files\IIS Express iisexpress.exe /path:C:\Windows\Microsoft.NET\Framework\v4.0.30319\ASP.NETWebAdminFiles /vpath:/WebAdmin /port:12345 /clr:4.0 /ntlm
編譯時若出現報錯:“System.Configuration.StringUtil”不可訪問,因為它受保護級別限制,請將WebAdminPage.cs中代碼作如下修改:
//取消部份: string appId = StringUtil.GetNonRandomizedHashCode(String.Concat(appPath, appPhysPath)).ToString("x", CultureInfo.InvariantCulture); //新增加部份: Assembly sysConfig = Assembly.LoadFile(@"C:\Windows\Microsoft.NET\Framework\v4.0.30319\System.Configuration.dll"); Type sysConfigType = sysConfig.GetType("System.Configuration.StringUtil"); string appId = ((int)sysConfigType.GetMethod("GetNonRandomizedHashCode").Invoke(null, new object[] { String.Concat(appPath, appPhysPath), true })).ToString("x", CultureInfo.InvariantCulture);
這樣就可以按照命令生生成的網址進行訪問就可以了。如果像我一樣,操作系統為:WINDOWS 10,那么不好意思,生成的網站雖然能夠打開,但仍會報錯:
遇到錯誤。請返回上一頁並重試。
目前沒有找到解決方案,網上有說ASP.NET網站管理工具在WIN10下不被支持,到底為何暫時無解,若大家有知道的還請分享一下(CSDN有別人的求問貼:http://bbs.csdn.net/topics/391819719),非常感謝,我這里就只好換台電腦來運行ASP.NET管理工具網站了。
WCF服務端配置如下:
<connectionStrings> <add name="SqlConn" connectionString="Server=.;Database=aspnetdb;Uid=sa;Pwd=www.zuowenjun.cn;"/> </connectionStrings> <system.web> <compilation debug="true" targetFramework="4.5" /> <httpRuntime targetFramework="4.5"/> <membership defaultProvider="SqlMembershipProvider"> <providers> <clear/> <add name="SqlMembershipProvider" type="System.Web.Security.SqlMembershipProvider" connectionStringName="SqlConn" applicationName="/" enablePasswordRetrieval="false" enablePasswordReset="false" requiresQuestionAndAnswer="false" requiresUniqueEmail="true" passwordFormat="Hashed"/> </providers> </membership> </system.web> <system.serviceModel> <behaviors> <serviceBehaviors> <behavior name="Service1Behavior"> <serviceCredentials> <serviceCertificate findValue="ZwjCert" storeLocation="LocalMachine" storeName="TrustedPeople" x509FindType="FindBySubjectName" /> <userNameAuthentication userNamePasswordValidationMode="MembershipProvider" membershipProviderName="SqlMembershipProvider" /> </serviceCredentials> <serviceMetadata httpGetEnabled="true" httpsGetEnabled="true" /> <serviceDebug includeExceptionDetailInFaults="false" /> </behavior> </serviceBehaviors> </behaviors> <bindings> <wsHttpBinding> <binding name="Service1Binding"> <security mode="Message"> <message clientCredentialType="UserName"/> </security> </binding> </wsHttpBinding> </bindings> <serviceHostingEnvironment aspNetCompatibilityEnabled="true" multipleSiteBindingsEnabled="true" /> <services> <service name="WcfService1.Service1" behaviorConfiguration="Service1Behavior"> <endpoint address="" binding="wsHttpBinding" contract="WcfService1.IService1" bindingConfiguration="Service1Binding"> </endpoint> </service> </services> </system.serviceModel>
這里需注意幾點:
1.配置connectionString,連接到membership所需的數據庫;
2.配置membership,增加SqlMembershipProvider屬性配置;
3.配置serviceCredential,與第一種基本相同,不同的是userNameAuthentication的配置:userNamePasswordValidationMode="MembershipProvider",membershipProviderName="SqlMembershipProvider";
4.配置Binding節點<message clientCredentialType="UserName"/>,這與第一種相同;
客戶端引用WCF服務,查看生成的配置文件內容,需確保Binding節點有以下配置信息:
<security mode="Message"> <message clientCredentialType="UserName" /> </security>
最后使用WCF服務,使用代碼與第一種相同,唯一需要注意的是,傳入的UserName和Password均為ASP.NET網站管理工具中創建的用戶信息。
另外我們也可以采用membership+Form驗證,利用ASP.NET的身份驗證機制,要實現這種模式,是需要采用svc文件,並寄宿在IIS上,具體實現方法,參見:http://www.cnblogs.com/danielWise/archive/2011/01/30/1947912.html
由於WCF的驗證方法很多,本文無法一次性全部寫完,敬請期待續篇!