现在的位置: 首页 > 综合 > 正文

[WCF安全系列]服务凭证(Service Credential)与服务身份(Service Identity)

2011年01月29日 ⁄ 综合 ⁄ 共 7238字 ⁄ 字号 评论关闭

采用TLS/SSL实现Transport安全的情况下,客户端对服务证书实施认证。但是在默认情况下,这种认证仅仅是确保服务证书的合法性(通过数字签名确保证书确实是由申明的CA颁发)和可信任性(证书或者CA证书存储于相应的可信赖存储区)。而WCF提供服务证书并不限于此,客户端对服务认证的模式应该是这样的:服务端预先知道了服务的身份,在进行服务调用之前,服务端需要提供相应的凭证用以辅助客户端确认调用的服务具有预先确定的身份。对于这样的服务认证模式,具有两个重要的概念,即服务凭证和服务身份。

目录:
一、服务凭证(Service Credential)
二、服务身份(Service Identity)
三、服务凭证协商(Service Credentials Negotiation)

一、服务凭证(Service Credential)

认证就是通过对对方提供的凭证进行检验以确定对方身份的一个过程,从这个意义上讲服务认证和客户端认证并没有本质的区别。但有服务认证确实有一点和客户端认证不同:客户端在对服务进行认证之前就预先确定了服务应当具有的身份。而在真正进行服务调用的时候,客户端要求服务提供相应的凭证。而客户端根据这个凭证和实现确定的身份进行比较,从而确定当前正在调用的服务正是自己希望调用的那个。

通过上面一节的介绍,我们已经知道了客户端具有多种形式的凭证类型,但是服务凭证具有两种典型的类型:Windows凭证和X.509证书。服务凭证的类型决定了认证方式,所以服务认证通过Windows认证或者对X.509证书的检验来实现。

而Windows认证具有两种具体的实现,即KerberosNTLM。通过前面对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实际上是一个抽象类,它具有如下几个常用的子类:SpnEndpointIdentityUpnEndpointIdentityX509CertificateEndpointIdentityRsaEndpointIdentityDnsEndpointIdentity,分别表示不同的服务身份类型。这些个具体的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:     ...

抱歉!评论已关闭.