在采用TLS/SSL实现Transport安全的情况下,客户端对服务证书实施认证。但是在默认情况下,这种认证仅仅是确保服务证书的合法性(通过数字签名确保证书确实是由申明的CA颁发)和可信任性(证书或者CA证书存储于相应的可信赖存储区)。而WCF提供服务证书并不限于此,客户端对服务认证的模式应该是这样的:服务端预先知道了服务的身份,在进行服务调用之前,服务端需要提供相应的凭证用以辅助客户端确认调用的服务具有预先确定的身份。对于这样的服务认证模式,具有两个重要的概念,即服务凭证和服务身份。
目录:
一、服务凭证(Service Credential)
二、服务身份(Service Identity)
三、服务凭证协商(Service Credentials Negotiation)
一、服务凭证(Service Credential)
认证就是通过对对方提供的凭证进行检验以确定对方身份的一个过程,从这个意义上讲服务认证和客户端认证并没有本质的区别。但有服务认证确实有一点和客户端认证不同:客户端在对服务进行认证之前就预先确定了服务应当具有的身份。而在真正进行服务调用的时候,客户端要求服务提供相应的凭证。而客户端根据这个凭证和实现确定的身份进行比较,从而确定当前正在调用的服务正是自己希望调用的那个。
通过上面一节的介绍,我们已经知道了客户端具有多种形式的凭证类型,但是服务凭证具有两种典型的类型:Windows凭证和X.509证书。服务凭证的类型决定了认证方式,所以服务认证通过Windows认证或者对X.509证书的检验来实现。
而Windows认证具有两种具体的实现,即Kerberos和NTLM。通过前面对Kerberos和NTLM的介绍,你应该知道只有Kerberos支持双向认证,而NTLM则不能。因此,只有在基于域(Domain)的网络环境中,基于Windows认证的服务认证才是可行的。而在工作组(Work
Group)环境中,我们推荐使用基于证书的服务认证。
服务认证方式的选择决定于客户端认证采用的方式,基本的策略是这样的:如果采用Windows认证的方式对客户端实施认证,服务认证同样采用Windows认证。基于X.509证书的认证在非Windows客户端认证下被采用。进一步地,如果客户端凭证类型为Windows,那么WCF采用执行服务寄宿进程的Windows帐号对应的Windows凭证作为服务凭证。如果其他非Windows凭证作为客户端凭证,你必须为服务显式地指定一个X.509证书作为服务凭证。这也是为何在前面演示的实例中,当NetTcpBinding采用Transport安全模式,客户端凭证被设置成None时,为何需要为服务指定一个X.509证书作为服务凭证的原因。
在WCF的应用编程接口中,具有一个重要的服务行为ServiceCredentials。这个类并不简单象它的名称所表示的那样用于进行服务凭证的设置,实际上需要在服务端执行的很多认证、授权行为都是通过ServiceCredentials(或者ServicePointManager的RemoteCertificateValidationCallback回调)来实现的。而在这里,我们暂时只关心如何通过ServiceCredentials为服务指定一个X.509证书作为服务凭证。关于这一点,已经在前面作过介绍了。
如果服务采用基于X.509证书作为服务凭证,客户端对服务的认证过程实际上分为两个阶段。第一个阶段是验证证书的合法性,在默认的情况下会采用ChainTrust认证模式,不过可以通过终结点行为ClientCredentials(或者ServicePointManager的RemoteCertificateValidationCallback回调)来设置不同的认证模式。关于具体对服务证书认证模式的设置在前面的实例演示(《TLS/SSL在WCF中的应用[SSL over TCP]》和《TLS/SSL在WCF中的应用[HTTPS]》)中已经有过介绍了。当通过以第一阶段的认证之后,才会进入第二阶段的认证,即通过比较服务证书和事先确立的服务身份信息进行对照进而确定服务是否是客户端试图访问的服务,接下来讨论关于服务身份的话题。
二、服务身份(Service Identity)
我们知道终结点时WCF最为核心的概念,终结点通过类型ServiceEndpoint表示。终结点具有ABC三要素分别表示地址、绑定和契约,其中地址通过EndpointAddress表示。如果你对EndpointAddress有一定的了解,你应该清楚该类具有一个只读的Identity的属性,对应的类型为EndpointIdentity,相关定义如下面的代码片断所示。
1: public class ServiceEndpoint
2: {
3: //其他成员
4: public EndpointAddress Address { get; set; }
5: }
6: public class EndpointAddress
7: {
8: //其他成员
9: public EndpointIdentity Identity { get; }
10: }
我们通常所说的“调用某个服务”实际上应该是“调用服务的某个终结点”,而服务身份实际上也应该是“终结点身份”。与此对应的,通过ServiceEndpoint对象表示的终结点的身份通过Address的Identity属性来表示,而该属性的类型就是本节着重介绍的EndpointIdentity。在深入介绍EndpointIdentity之前,我们不妨先来看看它的定义。
1: public abstract class EndpointIdentity
2: {
3: //其他成员
4: public static EndpointIdentity CreateSpnIdentity(string spnName);
5: public static EndpointIdentity CreateUpnIdentity(string upnName);
6: public static EndpointIdentity CreateRsaIdentity(X509Certificate2 certificate);
7: public static EndpointIdentity CreateRsaIdentity(string publicKey);
8: public static EndpointIdentity CreateX509CertificateIdentity(X509Certificate2 certificate);
9: public static EndpointIdentity CreateDnsIdentity(string dnsName);
10:
11: public Claim IdentityClaim { get; }
12: }
13: public class SpnEndpointIdentity : EndpointIdentity
14: {
15: //省略成员
16: }
17: public class UpnEndpointIdentity : EndpointIdentity
18: {
19: //省略成员
20: }
21: public class DnsEndpointIdentity : EndpointIdentity
22: {
23: //省略成员
24: }
25: public class RsaEndpointIdentity : EndpointIdentity
26: {
27: //省略成员
28: }
29: public class X509CertificateEndpointIdentity : EndpointIdentity
30: {
31: //省略成员
32: }
服务身份声明通过属性IdentityClaim表示,这些信息是为最终的认证服务服务的。从上面的代码我们可能看出,EndpointIdentity实际上是一个抽象类,它具有如下几个常用的子类:SpnEndpointIdentity、UpnEndpointIdentity、X509CertificateEndpointIdentity、RsaEndpointIdentity和DnsEndpointIdentity,分别表示不同的服务身份类型。这些个具体的EndpointIdentity可以通过对应的静态方法CreateXxxIdentity创建。
我们先来介绍一下SpnEndpointIdentity和UpnEndpointIdentity。这两个EndpointIdentity是Windows认证下服务身份的两种表现形式。前者被称为服务主体名(SPN:Service Principal Name,以下简称SPN),另一种被称为用户主体名(UPN:User Principal Name,以下简称UPN)。
如果你对Kerberos有一定的了解,相信一定对SPN不会感到陌生。对于一个运行在域环境中某台机器上的服务,它能被访问它的客户端认证的先决条件是:客户端能够唯一标识该服务,而SPN就可以看作是这个标识符。在默认的情况下,如果服务寄宿进程在机器帐号(或者系统帐号,比如LocalService, LocalSystem, or NetworkService等)下,服务身份通过SPN表示;如果执行服务寄宿进程的是一个域用户帐户,则采用UPN表示服务身份。WCF中的SPN和UPN的格式如下。如果客户端预先指定SPN/UPN表示服务身份,它通过执行服务寄宿进程帐号对应的Windows凭证和SPN/UPN进行比较,从未确定服务运行在预先设定的机器或者某个域用户帐号下。
1: SPN:Host/<<HostName>> (Host/artech-win7-x64)
2:
3: UPN:<<DomainName>>/<<UserName>>(Microsoft/BillGates)或者
4: <<UserName>>@<<DomainName>> (BillGates@Microsoft)
如果采用X.509证书作为服务凭证,服务身份可以通过X509CertificateEndpointIdentity和RsaEndpointIdentity表示。而X509CertificateEndpointIdentity有具有两种表现形式,既可以直接采用X.509证书中的指纹作为服务身份标识,也可以采用为了存储区中某个证书的引用来表示。而RsaEndpointIdentity则将X.509证书的RSA密钥作为服务身份标识。如果客户端预先制定了相应的X509CertificateEndpointIdentity/RsaEndpointIdentity作为服务身份,它会通过将作为服务凭证的X.509证书与此进行比较进而确定服务是相应证书的真正拥有者。
而对于DnsEndpointIdentity,故名思义就是基于域名系统(DNS: Domain Name
System)的服务身份表现形式。如果采用X.509证书作为服务凭证,并且这个证书的主题名称是一个DNS,客户端可以采用DnsEndpointIdentity来对服务证书进行认证。在基于SPN的Windows认证下,并且SPN是基于一个DNS,客户端也可以采用DnsEndpointIdentity认证服务。换句话会说,对于如下如下表示的DnsEndpointIdentity和SpnEndpointIdentity,在Windows认证下具有相同的认证效果。
1: DnsEndpointIdentity:artech.com
2: SpnEndpointIdentity: host/artech.com
服务端和客户端的终结点都可以设置这个表示服务身份标识的EndpointIdentity,不过对于整个服务认证机制,EndpointIdentity之于服务端和客户端终结点具有不同的作用。服务端终结点设置的EndpointIdentity用于元数据发布,客户端终结点设置EndpointIdentity最终用于对服务的认证。
一般情况下,在进行服务寄宿的时候,终结点的EndpointIdentity无需指定,因为WCF会根据绑定采用的客户端凭证类型和寄宿进程运行的Windows帐号为你生成相应的EndpointIdentity。终结点的EndpointIdentity最终会成员元数据的一部分被写入服务的WSDL中。比如说,我们采用IIS的方式寄宿服务,终结点采用Transport模式的WS2007HttpBinding,EndpointIdentity对应在WSDL部分的内容将会如下面的XML片断所示。由于IIS(IIS
6或之后版本)在Network
Servier帐号下执行,所以默认会使用SPN作为服务身份标识(SPN中的Jinnan-Win7-X64为机器名称)。
1: <wsdl:definitions name="CalculatorService" targetNamespace="http://tempuri.org/">
2: ...
3: <wsdl:service name="CalculatorService">
4: <wsdl:port name="WS2007HttpBinding_ICalculator" binding="tns:WS2007HttpBinding_ICalculator">
5: <soap12:address location="https://jinnan-win7-x86/WcfServices/CalculatorService.svc"/>
6: <wsa10:EndpointReference>
7: <wsa10:Address>
8: https://jinnan-win7-x64/WcfServices/CalculatorService.svc
9: </wsa10:Address>
10: <Identity>
11: <Spn>host/Jinnan-Win7-X64</Spn>
12: </Identity>
13: </wsa10:EndpointReference>
14: </wsdl:port>
15: </wsdl:service>
16: </wsdl:definitions>
客户端通过添加服务引用或者直接使用SvcUtil.exe导入元数据生成客户端代码和配置的时候,WSDL中的服务身份标识会自动被写入配置中。上述六种不同形式的EndpointIdentity在配置中的表示如下面的XML片断所示。
1: <system.serviceModel>
2: <client>
3: <endpoint address="http://jinnan-win7-x86/calculatorservice1" binding="ws2007HttpBinding" contract="Artech.WcfServices.Contracts.ICalculator">
4: <identity>
5: <userPrincipalName value="jinnan@contoso.com"/>
6: <servicePrincipalName value="host/jinnan-win7-x86"/>
7: <certificate encodedValue="f332bf17db3abb8f9a9a2694ba2c75da701bef0f"/>
8: <certificateReference storeLocation="LocalMachine" storeName="My" x509FindType="FindBySubjectName" findValue="jinnan-win7-x86"/>
9: <rsa value="sdhjgr...djakjhg"/>
10: <dns value="jinnan-win7-x86"/>
11: </identity>
12: </endpoint>
13: </client>
14: </system.serviceModel>
如果你是通过单纯编程的方式来创建用于进行服务调用的终结点,你可以按照如下的方式手工创建相应的EndpointIdentity对象。在创建作为终结点地址的EndpointAddress对象时,作为构造函数的参数传入。一旦成功创建EndpointAddress对象,你就可以通过它的只读属性Identity获得你指定的EndpointIdentity。
1: EndpointIdentity identity = EndpointIdentity.CreateSpnIdentity(@"host\Jinnan-Win7-X86");
2: EndpointAddress address = new EndpointAddress(new Uri("http://jinnan-win7-x64/calculatorservice"),identity);
3: ServiceEndpoint endpoint = new ServiceEndpoint(ContractDescription.GetContract(typeof(ICalculator)),new WS2007HttpBinding(),address);
4: using (ChannelFactory<ICalculator> channelFactory = new ChannelFactory<ICalculator>(endpoint))
5: {
6: ICalculator calculator = channelFactory.CreateChannel();
7: double result = calculator.Add(1, 2);
8: ...
9: }
三、服务凭证协商
被用于调用服务的客户端终结点最终都关联到一个EndpointIdentity对象上,而该EndpointIdentity对象代表了客户端希望调服务的真实身份。客户端在正式向服务发送功能性消息之前,会根据服务端提供的服务凭证和这个EndpointIdentity对服务实施认证。如果服务凭证与客户端持有的服务身份相一致,则认证成功,并开始后续的消息交换,否则双方之间的交互到此为止。
在默认的情况下,正进行服务认证中客户端和服务端有一个“协商(Negotiation)”的过程。客户端通过此协商过程从服务端获取服务凭证,所以我们将这个协商机制成为“服务凭证协商(Service
Credentials
Negotiation)”。对于Transport安全模式,服务凭证协商过程总是会发生,但是对于Message安全模式,你可以通过编程或者配置避免服务凭证协商。
如果服务凭证不能通过协商的方式即时地传递给客户端,那么必然要通过另外的方式递交给它。对于Windows认证,需要客户端和服务端必须出于同一域中。而对基于X.509证书的服务凭证,需要实现安装到客户端。抑制服务凭证协商会因避免证书的传递而对安全性有所增强,但是也会因为需要额外的证书递交机制而带来额外的负担。如果你只需要拥有相应证书的客户端才能调用你的服务,不妨采用这种方式。
对于所有支持Message模式的绑定来说,只有基于WS的绑定(WSHttpBinding、WS2007HttpBinding和WSDualHttpBinding)支持服务凭证协商。而开启和关闭服务凭证协商可以通过设置MessageSecurityOverHttp类型的NegotiateServiceCredential属性来实现。
1: public class MessageSecurityOverHttp
2: {
3: //其他成员
4: public bool NegotiateServiceCredential {get; set; }
5: }
不论是在进行服务寄宿还是服务调用的时候,你都可以通过编程的方式来关闭服务凭证协商机制。具体的编程方式,可以参考如下的代码。
1: //服务寄宿代码
2: using (ServiceHost host = new ServiceHost(typeof(CalculatorService)))
3: {
4: WS2007HttpBinding binding = new WS2007HttpBinding( SecurityMode.Message);
5: binding.Security.Message.NegotiateServiceCredential = false;
6: host.AddServiceEndpoint(typeof(ICalculator), binding, "http://127.0.0.1:3721/calculatorservice");
7: host.Open();
8: ...
9: }
10: //客户端代码
11: EndpointAddress address = new EndpointAddress("http://jinnan-win7-x64/calculatorservice");
12: WS2007HttpBinding binding = new WS2007HttpBinding(SecurityMode.Message);
13: binding.Security.Message.NegotiateServiceCredential = false;
14: ServiceEndpoint endpoint = new ServiceEndpoint(ContractDescription.GetContract(typeof(ICalculator)), binding, address);
15: using (ChannelFactory<ICalculator> channelFactory = new ChannelFactory<ICalculator>(endpoint))
16: {
17: ICalculator calculator = channelFactory.CreateChannel();
18: double result = calculator.Add(1, 2);
19: ...
20: }
我们当然还是推荐采用配置的方式来控制服务凭证协商,在WSHttpBinding、WS2007HttpBinding和WSDualHttpBinding的<security>/<message>配置节点中,你可以找到negotiateServiceCredential配置属性,这是开启和关闭服务凭证协商的开关,相应的配置如下所示。
1: <system.serviceModel>
2: <bindings>
3: <ws2007HttpBinding>
4: <binding name="transportWS2007HttpBinding">
5: <security mode="Message">
6: <message clientCredentialType="UserName" negotiateServiceCredential="false"/>
7: </security>
8: </binding>
9: </ws2007HttpBinding>
10: </bindings>
11: ...
12: </system.serviceModel>
作者:蒋金楠
微信公众账号:大内老A
微博:www.weibo.com/artech
如果你想及时得到个人撰写文章以及著作的消息推送,或者想看看个人推荐的技术资料,可以扫描左边二维码(或者长按识别二维码)关注个人公众号(原来公众帐号蒋金楠的自媒体将会停用)。
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。