【實踐】WCF傳輸安全4:基於SSL的WCF對客戶端采用證書驗證


前一篇我們演示了基於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:

Books.cs 代碼
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; }
    }
}
IBookContract.cs 代碼
using System.ServiceModel;
using System.Collections.Generic;

namespace LxContracts
{
    [ServiceContract]
    public interface IBookContract
    {
        [OperationContract]
        List<Books> GetAllBooks();
    }
}

2、  程序集LxServices(包括服務類BookService.cs),需要引用項目中的程序集LxContracts:

BookService.cs 代碼
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程序集:

Program.cs 代碼
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();
            }
        }
    }
}
App.config 配置文件代碼
<?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。服務引用添加好之后,我們編寫調用代碼如下:

Program.cs 代碼
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 配置文件,在終結點的行為中指定我們客戶端的證書,如下:

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 方法。在這個方法中,根據證書的指紋來對客戶端提供的證書進行檢驗。

CustomCertificateVerification.cs 代碼
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證書方式的客戶端驗證,在學習這方面知識的時候,自己查找了很多資料,再此將這些開發的步驟和關鍵點記錄下來和大家分享,不足之處,希望大家多多指正。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM