【实践】WCF传输安全3:基于SSL的WCF对客户端验证
上文我们演示了,客户端对服务器端身份的验证,这一篇来简单演示一下对客户端身份的验证。比如我们发布的服务,只想让若干客户端调用和访问。这种情况应该怎么做呢,这就是今天要演示的客户端认证。
对客户端的认证基本分为三种:
1、 windows 身份验证
2、 用户名密码方式验证
3、 证书认证
我们今天主要用第二种方式来对客户端进行验证。
WCF的传输安全中,HttpClientCredentialType 提供了6种客户端凭证来体现服务端对客户端的认证方式,(以下对6种认证方式的解释,摘自蒋金楠《WCF技术剖析》):
None: 客户端无须指定用户凭证,即匿名认证。
Basic:采用Basic认证方式对客户端进行认证,这种方式客户端需要提供有效的用户名和密码,但仅采用较弱的方式对密码进行加密。当且仅当确定客户端和服务器端直接的连接绝对安全的情况下采用此方式。
Digest:Digest认证提供与Basic类似的认证功能,但是安全性有所提升,主要体现在并不是将用户名和密码直接进行网络传输,而是对其进行MD5 运算得到一个哈希码,最终传输的是该哈希码。
Ntlm:表示基于NTLM方式的windows集成认证。
Windows:表示使用windows集成认证,如果能够使用Kerberos,则直接采用Kerberos进行认证,否则才使用NTLM
Certificate:表示客户端的身份通过一个X.509数字证书表示,服务端通过校验证书的方式来确定客户端的真实身份。
Windows 身份的验证 基本上计算机都需要加入域的,需要域控制器来处理客户端的身份。有点复杂,我们就实验一简单的用户名密码方式来对客户端进行验证。说白了,就是服务器端提供证书,客户端通过该证书对服务器端进行身份的验证,服务器端根据客户端提供的用户名密码来判断是否能允许调用服务。
我们还用我们上一篇的例子就可以,将代码稍微改动一下,就可以实现。
首先,我们 将 Host_Server 中App.config 文件中 bindings 节点中
<transport clientCredentialType="None"/>
修改为:
<transport clientCredentialType="Basic"/>
这时候我们重新生成一下Host_Server,并使其运行后,更新一下Client1 的服务引用,再运行Client1,我们会发现服务调用失败,我们抛出这个错误提示,如下图:
由于我们修改basic方式对客户端进行认证,这时候我们的客户端就需要提供一个用户名和密码来访问这个服务,这个访问服务的密码是通过生成的客户端代理类 中的一个属性 ClientCredentials.UserName.UserName 和 ClientCredentials.UserName.Password 来提供的。但是我们在WCF服务中什么地方来验证客户端提供的用户名和密码呢?比如能够提供用户名 admin 和密码 123456 的客户端,我们都认为是合法的客户端,都允许和服务进行通信。我们在WCF服务端可以这样进行判断:
首先,在我们的服务类库LxServices中添加引用:System.IdentityModel 和 System.IdentityModel.Selectors,并增加一个自定义身份验证类 CustomIdentityVerification 使其继承 UserNamePasswordValidator类,并重写其中的 Validate 方法,代码如下:
using System; using System.IdentityModel; using System.IdentityModel.Selectors; using System.IdentityModel.Tokens; namespace LxServices { public class CustomIdentityVerification : UserNamePasswordValidator { public override void Validate(string userName, string password) { //这里可以对通过输入的用户名密码进行验证,比如可以在数据库中进行查询 if (userName != "admin" || password != "123456") { throw new SecurityTokenException("用户名密码错误!"); } } } }
然后,我们需要修改WCF宿主程序的配置文件App.config,使我们的这个自定义的验证类生效,因此在服务行为serviceBehaviors节点中增加:
<!--服务器端提供证书--> <serviceCredentials> <serviceCertificate storeName="My" x509FindType="FindBySubjectName" findValue="Lx-PC" storeLocation="LocalMachine"/> <clientCertificate > <authentication certificateValidationMode="None"/> </clientCertificate> <userNameAuthentication userNamePasswordValidationMode="Custom" customUserNamePasswordValidatorType="LxServices.CustomIdentityVerification,LxServices" /> </serviceCredentials>
以下是宿主程序完整的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"/> <clientCertificate> <authentication certificateValidationMode="None"/> </clientCertificate> <userNameAuthentication userNamePasswordValidationMode="Custom" customUserNamePasswordValidatorType="LxServices.CustomIdentityVerification,LxServices"/> </serviceCredentials> </behavior> </serviceBehaviors> </behaviors> <bindings> <wsHttpBinding> <binding name="LxWsHttpBinding"> <security mode="Transport"> <!--采用传输安全,客户端凭证=Basic--> <transport clientCredentialType="Basic"/> <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>
启动服务器端宿主程序后,更新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) { Console.WriteLine("请输入您的用户名:"); string userName = Console.ReadLine(); Console.WriteLine("请输入您的密码:"); string passWord = Console.ReadLine(); WCF.BookSrv.BookContractClient proxyClient = new WCF.BookSrv.BookContractClient(); proxyClient.ClientCredentials.UserName.UserName = userName; proxyClient.ClientCredentials.UserName.Password = passWord; List<Books> listBook = new List<Books>(); try { listBook = proxyClient.GetAllBooks(); listBook.ForEach(b => { Console.WriteLine("服务调用成功:"); 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(); } } }
我们的代码修改完成,我们运行一下客户端Client1,并输入用户名 admin 和密码 123456,可以成功调用服务:
我们输入一次错误的用户名和密码,比如输入 123,就不会成功调用服务,结果如下图所示:
至此,我们利用用户名密码方式对客户端进行验证的这个Demo ,就结束了,下一篇我们来演示一下传输安全中对客户端也使用证书方式的认证。