前一篇我們演示了基於SSL的WCF 對客戶端進行用戶名和密碼方式的認證,本篇我們演示一下服務器端對客戶端采用X.509證書的認證方式是如何實現的。
項目結構及服務代碼和前兩篇代碼是基本一樣的,為了大家看着方便,再從頭到尾進行一下演示。
一、制作證書:
本次制作證書和第一篇略有不一樣,主要為了演示證書的信任鏈關系,我們首先創建一個證書作為證書認證中心(CA)的根證書,我們還是利用MakeCert命令創建。在“開始”菜單中打開—>Microsoft Visual Studio 2010->Visual Studio 命令提示。
輸入:makecert -n "CN=LXCA" -r -sv D:\LXCA.pvk D:\LXCA.cer
這時候會彈出 “創建私鑰密碼”對話框 和 “輸入私鑰密碼”對話框,我們都輸入密碼“123456”。(這個密碼的作用就是今后對證書進行導入,導出等操作的時候,輸入的密碼),命令提示成功后,這時候會在我的計算機D盤生成兩個文件 LXCA.pvk 和 LXCA.cer。
這里說明一下,證書中三種常用的文件格式:.pvk 文件:私鑰文件;.cer 公鑰文件;還用一種擴展名為.pfx 文件是包含公鑰和私鑰的密鑰交換文件。
然后我們輸入命令:makecert -n "CN=Lx-PC" -ic D:\LXCA.cer -iv D:\LXCA.pvk -sr LocalMachine -ss My -pe -sky exchange
在彈出的 “輸入私鑰密碼” 對話框中輸入"123456"之后,提示創建成功。這就是我們的服務器端的證書(至於為什么輸入我本機的計算機名CN=Lx-PC,我們前文有講解),我們可以在運行MMC,在證書管理單元中看到該證書:
我們雙擊我們創建的這個證書,打開 “詳細信息” 標簽可以查看到證書的指紋,我們記錄下來,我的這個證書的指紋是:f5ccc32b77d5d12922e162a289c80c7e95d615ea
下面和原來一樣,將證書與計算機的端口進行綁定,在win7 下,依然使用 netsh 命令(netsh命令位於C:\Windows\System32 下),我們運行該命令,並輸入:
http add sslcert ipport=0.0.0.0:9000 certhash=f5ccc32b77d5d12922e162a289c80c7e95d615ea appid={BFC5621F-EF33-4009-AD7E-51EDDAEC4321}
這樣我們的證書就和9000端口綁定好了,如何對這兩個命令有疑問,請點擊這里,對這兩個命令和用法有詳細的介紹。
到這里,服務器端證書就創建好了,我們還需要將證書的頒發者 LXCA 納入到 “受信任的根證書頒發結構” 中(這一步主要是為了解決客戶端調用的時候產生的信任關系的問題,前文已有介紹)。我們 在MMC 證書管理單元中,在 ”受信任的根證書頒發機構” 節點右擊,選擇 “所有任務” -->“導入證書”,文件選擇D盤的 LXCA.cer 即可,如下圖:
之后,我們需要創建兩個客戶端用到的證書,命令同制作服務器端證書一樣:
Makecert -n "CN=Client1-PC" -ic D:\LXCA.cer -iv D:\LXCA.pvk -sr CurrentUser -ss My -pe -sky exchange
Makecert -n "CN=Client2-PC" -ic D:\LXCA.cer -iv D:\LXCA.pvk -sr CurrentUser -ss My -pe -sky exchange
我們創建兩個客戶端證書Client1-PC 和Client2-PC ,並放到 “當前用戶” 的存儲區,因為在實際項目中,客戶端的證書一般都存儲在當前用戶下,我們可以在MMC 證書管理單元中看到:
並記錄下兩個證書的指紋(后面要用到):
Client1-PC 指紋:4df6765861ca5b393127e4aea2d9dffc02ef7346
Client2-PC 指紋:5cd0b75bd54092f022e9c48eb971879dcbdc29c2
二、程序演示
同樣看一下項目結構,結構和服務代碼和前面幾篇用到的一樣:
1、 程序集 LxContracts(包括服務的數據契約Books.cs和操作契約IBookContract.cs)需要添加引用System.Runtime.Serialization 和System.ServiceModel:

using System.Runtime.Serialization; namespace LxContracts { [DataContract] public class Books { [DataMember] public int BookId { get; set; } [DataMember] public string BookName { get; set; } [DataMember] public float Price { get; set; } } }

using System.ServiceModel; using System.Collections.Generic; namespace LxContracts { [ServiceContract] public interface IBookContract { [OperationContract] List<Books> GetAllBooks(); } }
2、 程序集LxServices(包括服務類BookService.cs),需要引用項目中的程序集LxContracts:

using System; using System.Collections.Generic; using System.Linq; using System.Text; using LxContracts; namespace LxServices { public class BookService:IBookContract { public List<Books> GetAllBooks() { List<Books> listBooks = new List<Books>(); listBooks.Add(new Books() { BookId = 1, BookName = "讀者", Price = 4.8f }); listBooks.Add(new Books() { BookId = 2, BookName = "青年文摘", Price = 4.5f }); return listBooks; } } }
3、 程序集 Host_Server 是WCF服務的控制台宿主程序,需要引用 System.ServiceModel 和項目中LxContracts 程序集與 LxServices程序集:

using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.ServiceModel; using LxServices; namespace Host_Server { class Program { static void Main(string[] args) { using (ServiceHost host = new ServiceHost(typeof(BookService))) { host.Opened += delegate { Console.WriteLine("服務已經啟動,按任意鍵終止服務!"); }; host.Open(); Console.ReadKey(); host.Close(); } } } }

<?xml version="1.0"?> <configuration> <system.serviceModel> <behaviors> <serviceBehaviors> <behavior name="LxBehavior"> <!--不提供通過瀏覽器輸入https訪問元數據的方式--> <serviceMetadata httpsGetEnabled="false"/> <serviceDebug includeExceptionDetailInFaults="false"/> <!--服務器端提供證書--> <serviceCredentials> <serviceCertificate storeName="My" x509FindType="FindBySubjectName" findValue="Lx-PC" storeLocation="LocalMachine"/> </serviceCredentials> </behavior> </serviceBehaviors> </behaviors> <bindings> <wsHttpBinding> <binding name="LxWsHttpBinding"> <security mode="Transport"> <!--采用傳輸安全,客戶端憑證=Certificate--> <transport clientCredentialType="Certificate"/> <message clientCredentialType="None"/> </security> </binding> </wsHttpBinding> </bindings> <services> <service name="LxServices.BookService" behaviorConfiguration="LxBehavior"> <endpoint address="BookService" binding="wsHttpBinding" bindingConfiguration="LxWsHttpBinding" contract="LxContracts.IBookContract"/> <endpoint address="mex" binding="mexHttpsBinding" contract="IMetadataExchange"/> <host> <baseAddresses> <!--基地址是https--> <add baseAddress="https://Lx-PC:9000/"/> </baseAddresses> </host> </service> </services> <serviceHostingEnvironment aspNetCompatibilityEnabled="false"/> </system.serviceModel> </configuration>
注意:<transport clientCredentialType="Certificate"/> 我們對客戶端的認證方式修改成為了 Certificate
4、我們為項目添加一個Client1 控制台客戶端程序,在啟動 Host_Server 的情況下,我們添加服務引用:“https://lx-pc:9000/mex” ,服務引用命名為:“WCF.BookSrv”,並點擊 “高級” 將集合類型選擇為System.Collections.Generic.List。服務引用添加好之后,我們編寫調用代碼如下:

using System; using System.Collections.Generic; using System.ServiceModel; using System.ServiceModel.Security; using Client1.WCF.BookSrv; namespace Client1 { class Program { static void Main(string[] args) { WCF.BookSrv.BookContractClient proxyClient = new WCF.BookSrv.BookContractClient(); List<Books> listBook = new List<Books>(); try { listBook = proxyClient.GetAllBooks(); listBook.ForEach(b => { Console.WriteLine("客戶端1服務調用成功:"); Console.WriteLine("---------------"); Console.WriteLine("圖書編號:{0}", b.BookId); Console.WriteLine("圖書名稱:《{0}》", b.BookName); Console.WriteLine("圖書價格:{0}¥", b.Price); Console.WriteLine("---------------"); }); } catch (Exception ex) { Console.WriteLine("服務調用失敗,原因如下:"); Console.WriteLine(ex.Message); } Console.ReadKey(); } } }
生成之后,啟動服務器端 Host_Server,我們運行一下客戶端Client1,報錯,如下圖所示:
因為我們在服務器端對客戶端的認證方式為 Certificate,因此我們的客戶端必須要提供一個證書,才能正常訪問我們的服務,我們需要修改一下客戶端Client1的 app.config 配置文件,在終結點的行為中指定我們客戶端的證書,如下:

<?xml version="1.0"?> <configuration> <system.serviceModel> <!--在終結點行為中指定客戶端使用的證書--> <behaviors> <endpointBehaviors> <behavior name="cnt1Behavior"> <clientCredentials> <clientCertificate storeName="My" x509FindType="FindBySubjectName" findValue="Client1-PC" storeLocation="CurrentUser"/> </clientCredentials> </behavior> </endpointBehaviors> </behaviors> <bindings> <wsHttpBinding> <binding name="WSHttpBinding_IBookContract" closeTimeout="00:01:00" openTimeout="00:01:00" receiveTimeout="00:10:00" sendTimeout="00:01:00" bypassProxyOnLocal="false" transactionFlow="false" hostNameComparisonMode="StrongWildcard" maxBufferPoolSize="524288" maxReceivedMessageSize="65536" messageEncoding="Text" textEncoding="utf-8" useDefaultWebProxy="true" allowCookies="false"> <readerQuotas maxDepth="32" maxStringContentLength="8192" maxArrayLength="16384" maxBytesPerRead="4096" maxNameTableCharCount="16384" /> <reliableSession ordered="true" inactivityTimeout="00:10:00" enabled="false" /> <security mode="Transport"> <transport clientCredentialType="Certificate" proxyCredentialType="None" realm="" /> <message clientCredentialType="Windows" negotiateServiceCredential="true" /> </security> </binding> </wsHttpBinding> </bindings> <client> <endpoint address="https://lx-pc:9000/BookService" binding="wsHttpBinding" bindingConfiguration="WSHttpBinding_IBookContract" contract="WCF.BookSrv.IBookContract" behaviorConfiguration="cnt1Behavior" name="WSHttpBinding_IBookContract" /> </client> </system.serviceModel> </configuration>
再次運行客戶端Client1,發現服務調用成功:
我們在解決方案中再添加一個Client2的控制台客戶端,代碼和Client1 一樣,只不過配置文件中我們使用的是證書Client2-PC。我們運行Client2 發現服務也調用成功:
既然這樣的話,我們能不能像自定義用戶名和密碼認證一樣,對證書也進行檢驗呢。比如我們讓服務對客戶端證書進行認證的時候,讓持有證書 Client1-PC 的客戶端 不能訪問,但可以讓持有證書 Client2-PC 的客戶端訪問呢,是可以的,下面我們對服務進行一下改進
5、自定義客戶端的認證:
我們依然需要對 LxService 程序集添加引用 System.IdentityModel 和System.IdentityModel.Selectors ,然后在該程序集中添加一個自定義證書的驗證類 CustomCertificateVerification 並使其繼承 X509CertificateValidator ,重寫父類的 Validate 方法。在這個方法中,根據證書的指紋來對客戶端提供的證書進行檢驗。

using System; using System.IdentityModel; using System.IdentityModel.Tokens; using System.IdentityModel.Selectors; using System.Security.Cryptography.X509Certificates; namespace LxServices { public class CustomCertificateVerification : X509CertificateValidator { //只允許Client2 調用服務,Client1 調用失敗 public override void Validate(X509Certificate2 certificate) { if (certificate.Thumbprint.ToUpper() != "5CD0B75BD54092F022E9C48EB971879DCBDC29C2") { throw new SecurityTokenException("無效的證書"); } } } }
之后,我們修改一下宿主程序Host_Server 的App.config 配置文件,將原來的
<serviceCredentials> <serviceCertificate storeName="My" x509FindType="FindBySubjectName" findValue="Lx-PC" storeLocation="LocalMachine"/> </serviceCredentials>
修改為:
<serviceCredentials> <serviceCertificate storeName="My" x509FindType="FindBySubjectName" findValue="Lx-PC" storeLocation="LocalMachine"/> <clientCertificate> <authentication certificateValidationMode="Custom" customCertificateValidatorType="LxServices.CustomCertificateVerification,LxServices"/> </clientCertificate> </serviceCredentials>
使我們自定義的證書驗證類生效。
我們重新生成一下代碼,打開宿主程序Host_Server 的生成目錄Bin/Debug,直接運行Host_Server.exe(不要調試運行,否則服務器端會拋出異常)。然后運行Client1 客戶端,我們會發現 客戶端 Client1 已經無法調用服務了:
我們運行一下客戶端Client2,發現服務依然能夠調用成功:
三、總結:
基於SSL的WCF傳輸安全實踐,我們基本演示完成了,分別包括的匿名客戶端訪問,自定義用戶名和密碼客戶端驗證,使用X.509證書方式的客戶端驗證,在學習這方面知識的時候,自己查找了很多資料,再此將這些開發的步驟和關鍵點記錄下來和大家分享,不足之處,希望大家多多指正。