【实践】WCF传输安全2:基于SSL的WCF匿名客户端

这一篇我们利用上一篇制作的证书,来演示一个基于SSL的WCF服务,客户端需要验证服务器端的身份,服务器端不对客户端进行任何验证,即匿名客户端。

一、项目结构

为了演示方便,把项目分成了6层,首先说明一下项目的结构:

程序集名称 引用 简单说明
Client1   控制台客户端1,调用采用控制台自宿主的WCF
Client2   控制台客户端2,调用采用IIS宿主的WCF
Host_Server

System.ServiceModel
LxContracts(项目中)
LxServices (项目中)

控制台服务端采用控制台宿主WCF
HostWeb_Server

System.ServiceModel
LxContracts(项目中)
LxServices (项目中)

空的ASP.NET 网站,只包含一个BookSrv.svc文件
LxContracts

System.ServiceModel
System.Runtime.Serialization

WCF 的数据契约和操作契约
LxServices LxContracts(项目中) WCF 服务实现
















二、代码说明:

1、类库LxContracts(包括数据契约Books.cs和操作契约IBookContract.cs 文件) 

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 文件)

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 (控制台宿主)

我们的服务代码已经写好了,现在我们对该服务进行一下宿主,首先采用控制台宿主,为解决方案添加一个Host_Server 的控制台程序,我们服务的绑定方式采用wsHttpBinding 方式。并修改其App.config 文件,代码如下:

宿主程序 App.config 代码
<?xml version="1.0" encoding="utf-8" ?>
<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">
            <!--采用传输安全,客户端凭证=none(匿名客户端)-->
            <transport 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" multipleSiteBindingsEnabled="true" />
  </system.serviceModel>
</configuration>

和原来未基于SSL的WCF服务配置http 变为了https,至于基地址为什么写 计算机名这个原因,我们下面会进行解释。

宿主程序 Main函数代码
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();
            }
        }
    }
}

我们生成一下Host_Server,右击该项目选择 “启动新实例”,控制台宿主程序可以进行启动,如下图所示:

4、Client1:(控制台客户端1 对 控制台宿主的WCF 进行调用)

我们来调用一下新建控制台应用程序Client1,并添加服务引用,输入: https://lx-pc:9000/mex 之后,点击 “发现” 按钮(此时保证服务器端已经启动),会找我们发布的服务,命名为:WCF.BookSrv, 选择“高级“,集合类型选择: System.Collections.Generic;点击"确定",成功添加该服务引用。

这里需要解答一下上一篇做证书的时候为什么要将我们的证书 导入到 受信任人 或者 受信任的根证书颁发机构中,如果不这样做的话,我们的证书就是不可信任的,在客户端添加引用的时候,会有这样一个提示 “颁发此安全证书的公司不是您信任的公司”

 

由于我们在制作证书的时候,进行了此步骤的操作,所以不会产生该问题。

我们编写一下Client1客户端代码:

Client1 Main 函数代码
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>();
            listBook = proxyClient.GetAllBooks();
            Console.WriteLine("客户端1 调用自宿主服务器端WCF");
            listBook.ForEach(b =>
            {
                Console.WriteLine("---------------");
                Console.WriteLine("图书编号:{0}", b.BookId);
                Console.WriteLine("图书名称:《{0}》", b.BookName);
                Console.WriteLine("图书价格:{0}¥", b.Price);
                Console.WriteLine("---------------");
            });
            Console.ReadKey();
        }
    }
}

客户端 Client1 添加引用后,自动产生的App.config 代码:

Client1 App.config 代码
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <system.serviceModel>
        <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="None" 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"
                name="WSHttpBinding_IBookContract" />
        </client>
    </system.serviceModel>
</configuration>

  好了,我们在启动 服务器端的情况下,运行一下 我们的Client1 客户端,我们会发现服务调用成功了,没有任何的问题: 

5、利用IIS宿主发布该服务:

我们在项目中添加一个 命名为 “HostWeb_Server” 的Asp.net 空网站,引用项目中的“LxContracts程序集”和“LxServices 程序集”,并添加一个“BookSrv.svc” 文件,删除“BookSrv.Svc.cs” 文件,右击“BookSrv.svc”文件,选择“查看标记”,将里面的内容修改为:

<%@ ServiceHost Language="C#" Service="LxServices.BookService" %>

并修改网站的webconfig 文件 为如下代码,之后,生成我们的网站。

Web.Config 文件代码
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <system.web>
    <compilation debug="true" targetFramework="4.0" />
  </system.web>
  <system.serviceModel>
    <behaviors>
      <serviceBehaviors>
        <behavior name="LxBehavior">
          <!--提供通过浏览器输入https访问元数据的方式-->
          <serviceMetadata httpsGetEnabled="true" />
          <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">
            <transport clientCredentialType="None" />
          </security>
        </binding>
      </wsHttpBinding>
    </bindings>

    <services>
      <service name="LxServices.BookService" behaviorConfiguration="LxBehavior">
        <endpoint address="" binding="wsHttpBinding" bindingConfiguration="LxWsHttpBinding" contract="LxContracts.IBookContract" />
      </service>
    </services>
    <serviceHostingEnvironment aspNetCompatibilityEnabled="false" multipleSiteBindingsEnabled="true" />
  </system.serviceModel>
  <system.webServer>
    <defaultDocument>
      <files>
        <remove value="index.html" />
        <remove value="default.aspx" />
        <remove value="iisstart.htm" />
        <remove value="index.htm" />
        <remove value="Default.asp" />
        <remove value="Default.htm" />
        <add value="BookSrv.svc" />
      </files>
    </defaultDocument>
  </system.webServer>
</configuration>

接下来,打开IIS,新建网站,命名为 LxWCFSSL,程序池选择“ASP.NET v4.0”,物理路径选择我们解决方案中 HostWeb_Server 的目录,绑定类型选择 https,端口默认 433,证书选择 Lx-PC,如下图:

点击“确定” 按钮之后,点击我们新建的网站,在SSL 设置中,选择“要求SSL”,客户端证书选择 忽略,

这时候,我们就在IIS中部署好了我们的这个基于SSL的WCF站点。细心的人这时候可以发现,如果再次运行netsh 命令,结果如下:

是不是发现我们建立的证书又跟443端口进行了绑定了呢。

6、Client2 (控制台客户端2 对 IIS宿主的WCF 进行调用)

我们在解决方案中添加 控制台程序Client2 对刚刚在IIS中部署的WCF进行调用。添加服务引用,我们输入:https://127.0.0.1/BookSrv.svc,会弹出一个警告对话框;

我们先不去理他,点击是,并完成添加服务引用。

我们仍然用Client1 中的客户端代码进行 服务的调用,这时候会发生一个异常,如图:

这是为什么呢?这就是上一篇中 建立证书的时候,为什么要默认计算机名称的问题。我们把证书加入到 可信任的颁发机构的时候 颁发者 是Lx-PC,因为127.0.0.1 和 Lx-PC 不匹配,所以无法建立信任关系,因此会产生该异常。那意思是不是就得写计算机名称,而不能写 ip 了呢,其实我们这个是一个Win7 下的Demo,如果在局域网证书服务器发布的证书或者购买的第三方机构的证书,应该是不会有此问题的,一般来讲,证书 的CN 应该是我们的 网站的域名 比如:http://wwww.xxx.com,两者保持一致就不会产生这个问题了,这块没有实验,只是个人的理解。

那怎么解决这个问题呢?有两种方法:

1) 引用服务的时候 更改为:https://Lx-Pc/BookSrv.svc,然后添加引用,运行我们的客户端,就会成功。

2) 这是网上找到的一个方法,就是 在验证服务器证书回调事件增加一方法,让它始终返回True,始终信任该证书。

我们在Client2 中增加一个ServerTrust的类,代码如下:

ServerTrust.cs 代码
using System.Security.Cryptography.X509Certificates;
using System.Net.Security;

namespace Client2
{
    public static class ServerTrust
    {
        public static void TrustSrv()
        {
            System.Net.ServicePointManager.ServerCertificateValidationCallback -= ValidateServerCertificate;
            System.Net.ServicePointManager.ServerCertificateValidationCallback += ValidateServerCertificate;
        }
        /// <summary>  
        /// 验证服务器端证书的回调方法
        /// 无论如何都相信该证书
        /// </summary>        
        private static bool ValidateServerCertificate(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
        {
            return true;
        }
    }
}

 修改一下Client2 代码: 

Cilent2 Main 函数代码
using System;
using System.Collections.Generic;
using System.ServiceModel;
using System.ServiceModel.Security;
using Client2.WCF.BookSrv;

namespace Client2
{
    class Program
    {
        static void Main(string[] args)
        {

            WCF.BookSrv.BookContractClient proxyClient = new WCF.BookSrv.BookContractClient();
            //强制信任服务器端凭证
            ServerTrust.TrustSrv();
            List<Books> listBook = new List<Books>();
            listBook = proxyClient.GetAllBooks();
            Console.WriteLine("客户端2 调用IIS宿主WCF");
            listBook.ForEach(b =>
            {
                Console.WriteLine("---------------");
                Console.WriteLine("图书编号:{0}", b.BookId);
                Console.WriteLine("图书名称:《{0}》", b.BookName);
                Console.WriteLine("图书价格:{0}¥", b.Price);
                Console.WriteLine("---------------");
            });
            Console.ReadKey();
        }
    }
}

  Client2 编译后运行也会成功调用我们部署的服务。 

个人认为第二种方法不可取,如果这样的话,客户端对服务器端的认证就失去了意义,这仅仅是解决该DEMO 的一个方式。

至此,我们的这个Demo 结束,我们也应该对证书的配置和基于SSL的WCF服务有所了解了,下一篇将在此代码基础上对客户端进行验证。

posted @ 2012-09-13 16:02  Rising_Sun  阅读(3048)  评论(1编辑  收藏  举报