【实践】WCF传输安全2:基于SSL的WCF匿名客户端
这一篇我们利用上一篇制作的证书,来演示一个基于SSL的WCF服务,客户端需要验证服务器端的身份,服务器端不对客户端进行任何验证,即匿名客户端。
一、项目结构
为了演示方便,把项目分成了6层,首先说明一下项目的结构:
程序集名称 | 引用 | 简单说明 |
Client1 | 控制台客户端1,调用采用控制台自宿主的WCF | |
Client2 | 控制台客户端2,调用采用IIS宿主的WCF | |
Host_Server |
System.ServiceModel |
控制台服务端采用控制台宿主WCF |
HostWeb_Server |
System.ServiceModel |
空的ASP.NET 网站,只包含一个BookSrv.svc文件 |
LxContracts |
System.ServiceModel |
WCF 的数据契约和操作契约 |
LxServices | LxContracts(项目中) | WCF 服务实现 |
二、代码说明:
1、类库LxContracts(包括数据契约Books.cs和操作契约IBookContract.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; } } }
using System.ServiceModel; using System.Collections.Generic; namespace LxContracts { [ServiceContract] public interface IBookContract { [OperationContract] List<Books> GetAllBooks(); } }
2、类库LxServices(包括服务类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 文件,代码如下:
<?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,至于基地址为什么写 计算机名这个原因,我们下面会进行解释。
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客户端代码:
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 代码:
<?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 文件 为如下代码,之后,生成我们的网站。
<?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的类,代码如下:
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 代码:
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服务有所了解了,下一篇将在此代码基础上对客户端进行验证。