【转】通过 WSE 3.0 中的可扩展策略框架保护您的 Web 服务
Web Services Enhancements (WSE) 3.0 是 Visual Studio® 2005 和 Microsoft® .NET Framework 2.0 的附件。它提供高级 Web 服务功能,有助于和不断发展的 Web 服务协议规范同步。
WSE 策略框架提供了一种机制,以描述 Web 服务需要执行的约束和要求。本文,我将介绍 WSE 中策略框架的工作方式。我将讨论 WSE 3.0 支持的安全方案,特别是 WSE 生成和使用的安全 SOAP 消息的有线格式。然后,我会探讨可用于扩展框架的机制,从而使您可以应用和执行自定义的约束和要求。我的示例代码将使用该功能来添加重播检测功能,以保护 SOAP 消息的接收方。
WSE 策略框架
WSE 策略框架描述了与 ASMX(ASP.NET Web 服务框架)或 WSE Web 服务进行通信的约束和要求。策略是一个部署时概念。策略的一个实例转换成运行时组件,这些组件在发送方应用这些要求,或者在接收方执行这些要求。在客户端和服务之间交换任何消息之前,发生此转换过程(称为策略编译)。
原子运行时组件被称作 SOAP 筛选器,它们在单个 SOAP 信封级别运行。它们主要负责检查或转换 SOAP 信封。例如,发送方的 SOAP 筛选器可能会对消息进行数字签名并为其添加一个 Security 标头,像 WS-Security 规范中定义的那样。同样,接收方的 SOAP 筛选器可以从 SOAP 信封中提取 Security 标头,并要求在将消息分配到应用程序代码之前成功验证它包含的数字签名。
一个策略包含一个有序的策略断言列表。每个策略断言定义一个对 Web 服务的要求。两个对应的运行时组件执行这些要求。在图 1 中,策略和策略断言部署时概念分别对应于管线和 SOAP 筛选器运行时概念。例如,策略中要求的顺序允许描述身份验证要求的断言在描述授权要求的断言之前生效,这样就可以在授权执行期间使用调用方的身份(在身份验证过程中确定)。策略内策略断言的顺序控制了运行时应用或执行有关要求的顺序。图 1 显示策略编译在管线中如何按照 SOAP 筛选器的顺序来保存表示成策略断言的要求的顺序。
图 1 通过 SOAP 筛选器的断言实现的策略
在策略编译过程中,每个策略断言最多可以生成四个 SOAP 筛选器。每个 SOAP 筛选器在不同的请求/响应消息交换阶段执行。两个不同的 SOAP 筛选器处理客户端上的输入消息和输出消息,两个筛选器处理服务上的输入消息和输出消息。策略断言生成的 SOAP 筛选器遵循策略中断言的顺序。用一个管线来表示这个输入筛选器和输出筛选器的集合。
虽然一个策略断言最多可以创建四个 SOAP 筛选器,但并非所有表示为策略断言的要求都需要请求/响应消息模式中所有这四个挂钩点。例如,重播检测要求只需要两个 SOAP 筛选器,用于客户端和服务上的传入消息。在这种情况下,不需要对传出消息进行特殊处理。
策略对象模型
了解策略框架背后的对象模型有助于阐述这些概念。PolicyAssertion 抽象基类声明了四个用于创建 SOAPFilters 的方法。执行这些方法可以处理客户端或服务上的请求或响应。在运行时,将调用其中两个方法(调用哪两个方法取决于策略断言应用于客户端还是服务)为管线提供 SOAPFilters。这些方法中的任何一个都可能返回空,这表示断言在消息交换的这个点上不需要任何处理。
public abstract class PolicyAssertion{ public abstract SOAPFilter CreateClientInputFilter( FilterCreationContext context); public abstract SOAPFilter CreateClientOutputFilter( FilterCreationContext context); public abstract SOAPFilter CreateServiceInputFilter( FilterCreationContext context); public abstract SOAPFilter CreateServiceOutputFilter( FilterCreationContext context); ... }
SOAPFilter 是一个运行时类,负责 SOAP 消息的原子转换。其约定非常简单:必须实现的唯一方法接受对 SOAPEnvelope 进行检查或转换,并且返回 SOAPFilterResult。SOAPFilterResult 指明管线应继续处理还是终止。该终止功能仅在涉及处理基础结构消息的高级方案(例如,WS-Reliability 协议消息)中有用,不能直接分配到 Web 服务类:
public abstract class SOAPFilter { public abstract SOAPFilterResult ProcessMessage( SOAPEnvelope envelope); ... }
PolicyAssertion 被安排在 Policy 类实例中,以捕获某个服务或代理所要求的需求集。然后,该 Policy 可以充当创建客户端管线或服务管线的工厂,如下所示:
public class Policy : IPipelineProvider { public Collection Assertions { get; } public Policy(params PolicyAssertion[] assertions) { } public Pipeline CreateClientPipeline( PipelineCreationContext context) { } public Pipeline CreateServicePipeline( PipelineCreationContext context) { } }
Pipeline 类代表已排序的 SOAPFilter 对象集合,用于处理传入消息或传出消息。同样,Pipeline 可用于自定义服务或服务代理的运行时行为。
应用策略
WSE 中的策略可在代码中以命令方式定义,也可以在外部 XML 文件中以声明方式定义。这两种方法都解决特定的情况。在代码中以命令方式定义策略时,您可以完全控制对为了使用服务而需要满足的要求进行的定义。分发编译的应用程序时,管理员不能更改这些要求。
在 XML 文件中声明策略解决相反的情况:开发人员在代码中只提供应用程序逻辑,将指定策略要求的责任委托给管理员。当策略要求依赖于部署环境且不能在开发时确定时,该方法很有用。例如,同一个 Web 服务可以向 Windows 域和 Internet 的用户公开。在 Windows 域中,安全要求可以使用 Kerberos。同时,在 Internet 上可使用用户名/密码和 X.509 证书。
WSE 定义了一个 PolicyAttribute,该属性可添加到一个代表 ASMX Web 服务或服务代理的类中以应用特定策略:
[WebService] [Policy("HelloWorldPolicy")] public class HelloWorld : WebService { [WebMethod] public string Hello(string request) { return String.Format("Hello, {0}", request); } }
在本示例中,名为 HelloWorldPolicy 的策略应用于 HelloWorld Web 服务。在应用程序域中首次实例化服务类时,WSE 运行库将该策略名称解析为一个 Policy 实例,该实例在配置文件中引用的 XML 文件中声明。然后,该 Policy 实例将编译到一个包括传入消息和传出消息的 SOAPFilters 的管线中。所有指向该 ASMX Web 服务的 SOAP 请求和响应将通过该管线得到处理。Web 方法作用域不支持 PolicyAttribute。
配置文件中提供包含策略声明的 XML 文件的名称,如下所示:
<configuration> <Microsoft.Web.services3> <policy fileName="policies.config"/> </Microsoft.Web.services3> </configuration>
包含策略的 XML 文件的架构允许您声明任意数量的命名策略。这就允许同一个应用程序域中(因此共享一个配置文件)运行的不同服务使用不同的策略。例如,该 policies.config 文件包含 HelloWorldPolicy 和 Policy17 这两个策略的定义,如下所示:
<policies XMLns="http://schemas.Microsoft.com/WSE/2005/06/policy"> <policy name="HelloWorldPolicy"> <usernameForCertificateSecurity/> </policy> <policy name="Policy17"> <kerberosSecurity/> </policy> </policies>
每个策略只有一个策略断言:一个使用 usernameForCertificateSecurity 断言,另一个使用 kerberosSecurity 断言。这两个断言内置在 WSE 3.0 中,并且提供额外的配置选项。
PolicyAttribute 还可用于将策略应用于客户端上的服务代理。通过在代表 ASMX 代理的类上添加属性,可以实现该目的。然而,ASMX 代理类通常是用 Visual Studio 环境的“添加 Web 引用”功能或 wsdl.exe 工具自动生成的。这样,每次重新生成 PolicyAttribute 时,都必须将其添加到该类中,这很不方便。因此,将策略分配给客户端代理的推荐方式是调用其上的 SetPolicy 方法,如下所示:
HelloWorld serviceProxy = new HelloWorld(); serviceProxy.SetPolicy("HelloWorldPolicy");
SetPolicy 是由 WebServicesClientProtocol 类提供的,ASMX 代理类(在本示例中是 HelloWorld)必须从该类派生。WSE 与 Visual Studio 环境的“添加 Web 引用”功能紧密集成,从而确保所有新生成的 ASMX 代理类都是针对支持 WSE 的项目从 WebServicesClientProtocol 派生的。
当您希望让管理员决定策略的细节时,在代码中按名称引用策略是最好的办法。然而,如果您希望在代码中完全控制策略,您可以通过以下两种方式实现这一目标:您可以使用 PolicyAttribute 指明策略类的公共语言运行库(common language runtime,CLR)类型,也可以使用 SetPolicy 方法提供一个 Policy 对象的实例。
在图 2 中,MyPolicy 类从 Policy 基类派生,并用构造函数中的一个 KerberosAssertion 实例填充 Assertions 集合,从而在代码中创建一个完全指定的策略。接下来,将 PolicyAttribute 应用于 ASMX 服务类,以指明遵循 Policy 约定的类的 CLR 类型。创建服务实例时,默认的构造函数将通过 WSE 运行库实例化该类。产生的 Policy 实例被编译到管线。图 3 概括在 ASMX 和 SOAPClient/SOAPService 服务和代理上设置策略的推荐方法和可用方法。
图2:
class MyPolicy : Policy { public MyPolicy() : base() { this.Assertions.Add(new KerberosAssertion()); } } [WebService] [Policy(typeof(MyPolicy))] public class HelloWorld : WebService { [WebMethod] public string Hello(string request) { { return String.Format("Hello, {0}", request); } }
Service/Proxy | PolicyAttribute with Policy Name | PolicyAttribute with Policy CLR Type | SetPolicy Method with Policy Name | SetPolicy Method with Policy Instance |
---|---|---|---|---|
ASMX Service | ||||
ASMX Proxy | ||||
SoapSender | ||||
SoapReceiver | ||||
Recommended Available Not Applicable |
安全断言
WSE 3.0 中的策略框架提供了解决常见情况的六个现成的断言,有助于保护 Web 服务的安全。通常,这些断言使用 WS-Security 和 WS-SecureConversation 机制提供消息完整性、保密性以及身份验证。图 4 概括这六个断言提供的功能。
Policy Assertion | Integrity | Confidentiality | Secure Conversation | Client Authentication | Service Authentication |
---|---|---|---|---|---|
UsernameForCertificateAssertion | Username/password | X.509 | |||
MutualCertificate10Assertion | X.509 | X.509 | |||
MutualCertificate11Assertion | X.509 | X.509 | |||
AnonymousForCertificateAssertion | X.509 | ||||
UsernameOverTransportAssertion | Username/password | ||||
KerberosAssertion | Kerberos |
使用 WSE 提供的六个安全断言中的任何一个都有利于以后与 Windows Communication Foundation 互操作。目前,使用任何 WSE 3.0 断言保护的消息与 Windows Communication Foundation Beta 2 版本都是有线级别兼容的。如果您准备编写一个需要调用其他平台上编写的服务的 WSE 客户端应用程序,或者其他平台上创建的客户端将使用您的 WSE Web 服务,了解这些断言使用的 WS-* 规范的功能以及 Security SOAP 标头内部元素的实际布局是非常重要的。
除 UsernameOverTransportAssertion 外,所有安全断言都支持完整性、保密性和安全对话。完整性功能允许消息的接收方检查消息在传递过程中是否被修改。这是通过对消息进行数字签名以及接收方验证该签名实现的。WS-Security 标准要求数字签名遵循 XML 数字签名标准。该标准支持对 XML 文档(在本例中是 SOAP 信封)的选定部分进行签名。WSE 中的安全断言允许您确定需要对 SOAP 信封的哪些部分进行签名。默认情况下,所有 WS-Addressing SOAP 标头、包含应用程序负载的 SOAP 正文,以及 Security 标头中的时间戳都是由发送方签名的,并且要求接收方对它们进行签名。
保密性功能使消息发送方可以确保消息内容仅对预定的接收方可见。这是通过加密消息实现的。WS-Security 要求消息的加密遵循 XML 加密标准,该标准支持仅对 XML 文档的选定元素加密。WSE 中的标准断言仅支持对 SOAP 信封的正文加密,这是最常见的方案。在您决定不用纯文本方式表示正文时,这是默认行为。
除 UsernameOverTransportAssertion 外,WSE 中的所有断言都允许您自定义签名消息的部分内容,并且确定是否应该对消息正文加密。由于这些功能是由一个公共的 SecurityPolicyAssertion 基类处理的,因此所有断言的 API 都相同。
包含该断言的策略应用于服务类之后,这些保护要求将统一应用于服务的所有 Web 方法。更高级的方案可能要求:在由服务公开的各个操作之间,保护的要求有差异。两种最常见的方案与性能和互操作性有关。WSE 允许您区分由一个策略处理的各个操作的保护要求。为此,服务的各个操作是用请求消息(在 WS-Addressing 规范中定义)Action 标头的值唯一标识的。(WSE SDK 包含了这样一个示例:它阐述如何将 SOAP 正文的第一个子元素用作密钥来确定您的保护要求。)例如,如果您希望加密所有操作(uri:hello 操作标识外的除外)的 SOAP 消息的正文,则可以编写如下代码:
KerberosAssertion assertion = new KerberosAssertion(); assertion.Protection.DefaultOperation.Request.EncryptBody = true; assertion.Protection.DefaultOperation.Response.EncryptBody = true; OperationProtectionRequirements operationRequirements = new OperationProtectionRequirements(); operationRequirements.Request.EncryptBody = false; operationRequirements.Response.EncryptBody = false; assertion.Protection.Operations["uri:hello"] = operationRequirements;
每当您向消息添加一个自定义的 SOAP 标头时,您可能都希望使用策略为它定义保护要求。WSE 安全断言支持为自定义的 SOAP 标头指定完整性要求。如果您的标头具有命名空间 uri:example 和本地名称 myHeader,并且仅在请求上出现,则您可以要求用以下代码对它进行签名:
assertion.Protection.DefaultOperation.Request.CustomSignedHeaders.Add( new XMLQualifiedName("myHeader", "uri:example"));
通过使用外部 XML 策略文件,您可以控制所有这些完整性和保密性要求。例如,图 5 中显示的策略文件定义了一个策略,该策略对所有操作进行统一签名和加密,那些具有与 uri:hello 等效的请求操作的操作除外。
1:<policies>
2:<policyname="HelloWorldPolicy">
3:<kerberosSecurity>
4:<protection>
5:<requestencryptBody="True"/>
6:<responseencryptBody="True"/>
7:<faultencryptBody="False"/>
8:</protection>
9:<protectionrequestAction="uri:hello">
10:<requestsignatureOptions="IncludeAddressing, IncludeSoapBody"
11:encryptBody="False">
12:<signedHeadername="myHeader"namespace="uri:example"/>
13:</request>
14:<responsesignatureOptions="IncludeAddressing, IncludeSoapBody"
15:encryptBody="False"/>
16:<faultsignatureOptions="IncludeNone "encryptBody="False"/>
17:</protection>
18:</kerberosSecurity>
19:</policy>
20:</policies>
除 UsernameOverTransportAssertion 外,WSE 中的所有安全断言都支持按照 WS-SecureConversation 1.1 规范建立安全对话。安全对话背后的含义是在客户端和服务之间建立会话。使用特定于所选安全断言的凭据来保护 WS-SecureConversation 定义的握手协议的安全。例如,在 UsernameForCertificateAssertion 中,客户端在握手期间使用用户名和密码凭据对服务进行身份验证,服务使用它的 X.509 证书对客户端进行身份验证。在握手结束时,服务向客户端发出一个 SecurityContextToken。该令牌包含一个对称密钥,客户端和服务在交换后续消息时将使用它。
大多数情况下,对多个消息交换使用一个对称加密密钥比使用建立安全对话所用的凭据效率更高。如果您计划对服务的多个调用使用一个客户端代理实例,并且性能也很重要,则在策略中启用安全对话将受益最大。
建立安全对话是由安全断言生成的管线完全处理的。应用程序代码不必处理 WS-SecureConversation 在任何时候定义的握手消息。因此,在安全断言中启用安全对话很简单,只需打开它:
KerberosAssertion assertion = new KerberosAssertion(); assertion.EstablishSecurityContext = true;
在客户端和服务之间建立的安全上下文令牌与服务代理实例相关联,并且用于该代理上发送的所有消息。然而,在代理的有效期内,该令牌可能会过期。在这种情况下,当您尝试发送新请求时,默认情况下代理将引发异常。(如果不引发异常,服务无论如何都有可能拒绝消息。)
WSE 中的安全断言还有另一个功能,该功能允许安全对话一旦过期即可重建,这为代理的用户提供了无缝体验。然而,新安全对话与原来的安全对话没有任何加密关系。(在重建安全对话的过程中,不更新安全上下文令牌;而是获得一个新的安全上下文令牌。)由于使用该代理的应用程序可能依赖于安全对话的会话语义,因此默认情况下关闭重建行为。要启用重建行为,您只需在断言实例上再设置一个属性:
assertion.RenewExpiredSecurityContext = true;
从 XML 策略文件中同样可以完全访问控制安全对话行为的设置:
<policies> <policy name="HelloWorldPolicy"> <kerberosSecurity establishSecurityContext="True" renewExpiredSecurityContext="True" /> </policy> </policies>
用户名/密码身份验证
WSE 提供了两个断言,它们支持使用用户名/密码凭据进行客户端身份验证:UsernameForCertificateAssertion 和 UsernameOverTransportAssertion。UsernameForCertificateAssertion 提供使用 WS-Security 协议进行的客户端和服务器身份验证。UsernameOverTransportAssertion 仅提供使用 WS-Security 进行的客户端身份验证,并且依赖安全传输(如 HTTPS)来提供消息保护和服务器身份验证。
使用 UsernameForCertificateAssertion 时,服务必须配置了一个 X.509 证书及其相关的私钥。客户端需要配置有自己的用户名/密码凭据,以及服务的 X.509 证书(仅公钥)。假设客户端知道服务的 X.509 证书在带外。请注意,WSE 不支持在运行时使用像传输层安全(Transport Layer Security,TLS)和安全套接字层(Secure Sockets Layer,SSL)这样的协议获得服务的证书。
可以在 XML 策略文件中以声明的方式进行客户端或服务器凭据的配置:
<usernameForCertificateSecurity> <clientToken> <username username="tomasz" password="zsamot"/> </clientToken> <serviceToken> <x509 storeLocation="LocalMachine" storeName="My" findValue="CN=example.com" findType="FindBySubjectDistinguishedName" /> </serviceToken> </usernameForCertificateSecurity>
服务的证书在证书存储中引用,用户名/密码凭据直接嵌入到 XML 策略文件中。请注意,客户端的用户名/密码凭据仅在客户端需要。尽管存在这种可能,但您还是应该避免将密码存储在 XML 文件中。建议您不要在策略配置文件中包括 clientToken 元素,而是使用通过在代理上调用 SetClientCredential 方法提供用户名/密码凭据的命令性方法:
string username = ..., password = ...; HelloWorld HelloWorld = new HelloWorld(); HelloWorld.SetClientCredential(new UsernameToken(username, password));
这允许您在运行时查询应用程序用户的凭据并将其传递给代理,而无需将其存储在外部 XML 文件中。您还可以使用类似的机制在代码中提供 X.509 凭据,如果您想使用相同的代理类和策略与具有不同 X.509 证书的服务对话,这就能派上用场:
X509Certificate2 serviceCertificate; HelloWorld.SetServiceCredential( new X509SecurityToken(serviceCertificate));
如果您在代码中同时提供了客户端凭据和服务凭据,则您的策略可以精简为以下代码:
<policies> <policy name="HelloWorldPolicy"> <usernameForCertificateSecurity/> </policy> </policies>
那么 UsernameForCertificateAssertion 如何保护消息交换的安全呢?图 6 显示请求和响应消息的 Security 标头的布局示例。
图 6 用 UsernameForCertificateAssertion 保护的消息
为了保护请求消息的安全,WSE 创建了一个随机的对称密钥,使用服务的 X.509 证书中的公钥包装该密钥,并且使用该对称密钥对消息中要求完整性和保密性的部分进行签名和加密。以 xenc:EncryptedKey 元素的形式将对称密钥序列化到 Security 标头中。该元素内部的引用列表指向包含代表客户端凭据的用户名令牌的 xenc:EncryptedData 元素。这样,客户端的密码就不会暴露在网络上了。请求的 Security 标头中的最后一个元素是 dsig:Signature,它对要求完整性的所有消息部分、时间戳以及 xenc:EncryptedData 中加密的纯文本 wsse:UsernameToken 元素进行签名。
请注意,服务的 X.509 证书不会出现在请求消息的 Security 标头中。由于客户端和服务都可以访问它,因此不需要。相反,使用了一个描述该证书属性的外部引用。
响应消息使用客户端在请求中传递给服务的对称密钥对消息进行签名和加密。响应的 Security 标头包含时间戳、指向加密消息部分的 xenc:ReferenceList,以及对时间戳和要求完整性保护的消息部分进行签名的签名。
UsernameForCertificateAssertion 对以下事实进行验证:已经使用请求所用的对称密钥对响应进行了签名和加密。这证明响应是由 X.509 证书的所有者发送的,在请求中加密了该证书的对称密钥,这是因为,只有关联私钥的持有者才能解密对称密钥。该机制使断言可以在客户端提供服务器身份验证保证。
根据完整性和保密性要求,以及断言配置,有线格式可能会有所不同。特别是,如果您更改对消息应用签名和加密的顺序,布局将有所不同。默认情况下,先对消息进行签名,然后再进行加密。使用 MessageProtectionOrder 属性,您还可以将断言配置成先对消息进行加密,然后再签名,或者对消息签名、加密,然后再对消息签名进行加密。
断言的另一个可配置方面与密钥派生有关。默认情况下不使用派生的密钥,不过您可以使用 RequireDerivedKeys 属性改变这种情况。打开该功能后,每次使用对称密钥对消息进行签名和加密时,都会先根据该根对称密钥计算派生的密钥,然后签名或加密使用新派生的密钥。在这种情况下,您将在 Security 标头中看到两个额外的元素,它们代表用于签名和加密的派生密钥令牌。
WSE 中的安全断言支持的最后一个功能是 WS-Security 1.1 中定义的签名确认。默认情况下关闭该行为,不过您可以在断言上使用 RequireSignatureonfirmation 属性,以要求请求的接收方确认响应中包括了来自请求的所有签名。在这种情况下,您将在响应的 Security 标头中看到额外的 wsse:SignatureConfirmation 元素。
WSE 中第二个提供使用用户名/密码凭据进行客户端身份验证的断言是 UsernameOverTransportAssertion。该断言依靠安全的传输协议(如 HTTPS)来提供完整性、保密性,以及服务器身份验证保证。WS-Security 功能仅用于将用户名/密码凭据从客户端传递到 SOAP 消息 Security 标头中的服务。WSE 并不要求应用该断言时的传输实际上是安全的,因此您在使用该断言时必须格外小心。如果传输不安全,密码将在网络上以纯文本形式出现。
UsernameOverTransportAssertion 的配置最简单。因为服务器身份验证不在 SOAP 级别执行,所以服务器上不需要任何凭据配置。您的 XML 策略文件可以象下面这样简单:
<policies> <policy name="HelloWorldPolicy"> <usernameForCertificateSecurity/> </policy> </policies>
在客户端上,您可以选择在 XML 策略文件或代码中指定客户端的用户名/密码凭据。出于安全原因,再次建议您在代码中提供凭据。由于服务器身份验证是在传输层处理的,因此不需要在客户端提供服务的 X.509 证书。考虑到上述所有情况,您的客户端 XML 策略与服务器端的策略看上去是一样的。
请注意,在应用 UsernameOverTransportAssertion 时,用户名和相关的密码是在请求消息的 Security 标头中以纯文本形式在网络上发送的。因此,在传输层为整个消息提供保密性是至关重要的。
证书身份验证
WSE 提供了三个支持基于 X.509 证书进行身份验证的安全断言。AnonymousForCertificateAssertion 允许客户端在使用其 X.509 证书验证服务时保持匿名。MutualCertificate10Assertion 和 MutualCertificate11Assertion 使用客户端和服务的 X.509 证书执行相互验证。它们之间的区别在于所使用的 WS-Security 规范功能。MutualCertificate10Assertion 不需要任何 WS-Security 1.1 功能,因此适应更广泛的互操作性目标。MutualCertificate-11Assertion 使用 WS-Security 1.1 将 xenc:EncryptedKey 元素作为令牌引用的功能,以便在确保请求/响应交换的安全性方面获得更好的性能。然而,这要求两个通信终结点都支持较新的 WS-Security 规范。
要使用 AnonymousForCertificateAssertion,客户端和服务都必须配置有服务的 X.509 证书。该证书可以通过 XML 策略文件或代码来提供。该机制与我针对 UsernameForCertificateAssertion 描述的机制相同。图 7 中显示的有线格式甚至更简单。
图 7 用 AnonymousForCertificateAssertion 保护的消息
请注意,请求消息 Security 标头的布局与 UsernameForCertificateAssertion 的类似,响应消息的 Security 标头与请求消息的相同。请求的 Security 标头中缺少了 xenc:EncryptedData 元素,该元素以 wsse:UsernameToken 形式包含客户端的用户名/密码凭据。这样,客户端就可以保持匿名,而服务是用其 X.509 证书验证的,可以使用加密密钥令牌对请求和响应进行签名和加密。由于响应消息包含对请求中加密密钥令牌的引用,因此 AnonymousForCertificateAssertion 要求通信双方都支持 WS-Security 1.1 规范。
通过添加使用 X.509 证书的客户端身份验证,MutualCertificate11Assertion 构建在 AnonymousForCertificateAssertion 之上。客户端证书可以在 XML 策略文件中提供(请参见 图 8)。
1:<mutualCertificate11Security>
2:<clientToken>
3:<x509storeLocation="CurrentUser"storeName="My"
4:findValue="CN=client.com"
5:findType="FindBySubjectDistinguishedName"/>
6:</clientToken>
7:<serviceToken>
8:<x509storeLocation="LocalMachine"storeName="TrustedPeople"
9:findValue="CN=service.com"
10:findType="FindBySubjectDistinguishedName"/>
11:</serviceToken>
12:</mutualCertificate11Security>
您还可以在 XML 文件中省略客户端证书的规范,使用前面介绍的 SetClientCredential 方法在代码中直接提供它:
X509Certificate2 clientCertificate = ...; HelloWorld HelloWorld = new HelloWorld(); HelloWorld.SetClientCredential( new X509SecurityToken(clientCertificate)};
图 9 显示将 MutualCertificate11Assertion 应用于交换的效果。响应消息与 AnonymousForCertificateAssertion 生成的消息相同,需要支持 WS-Security 1.1。
图 9 用 MutualCertificate11Assertion 保护的消息
与 AnonymousForCertificateAssertion 相比,使用 MutualCertificate11Assertion 时有两个额外的元素:代表客户端 X.509 证书的二进制安全令牌,以及第二个使用该令牌对主要消息签名进行签名(对消息中要求完整性的所有部分进行签名的签名)的签名。第二个签名的作用是,就像客户端证书用于对主要签名签署过的消息的相同部分进行签名那样操作。这是可能的,因为主要签名的序列化形式遵循 XML 数字签名规范,这意味着它已经包含代表要求完整性保护的消息部分的摘要值。因此,第二个签名被称作印记签名。计算印记签名通常没有计算主要签名代价那样昂贵,只是因为代表主要签名的 XML 通常比涵盖所有消息部分的 XML 少。
MutualCertificate10Assertion 获得与 MutualCertificate11Assertion 相同的身份验证和消息保护效果。区别在于它仅可以使用 WS-Security 1.0 功能来完成它的工作。好处是该断言将与更多的产品进行互操作。代价是性能的下降:保护完整的请求/响应交换需要四个公钥加密操作,而不是 MutualCertificate11Assertion 需要的三个。
MutualCertificate10Assertion 和 MutualCertificate11Assertion 的配置要求相同。但请注意,图 10 中显示的有线格式完全不同。
图 10 用 MutualCertificate10Assertion 保护的消息
请求包含带有客户端 X.509 证书的二进制安全令牌。该令牌用于直接对消息中要求完整性的所有部分进行签名,包括时间戳。加密方式与 AnonymousForCertificateAssertion 和 MutualCertificate11Assertion 中使用的方式相同:使用对服务的 X.509 证书加密的对称密钥令牌。
响应消息遵循的格式与请求类似。使用服务的 X.509 证书对消息进行签名,并使用为客户端 X.509 证书的公钥加密的对称密钥令牌对消息进行加密。然而,响应不包含带有服务的 X.509 证书的二进制安全令牌。这是因为客户端已经具备该证书(它用于帮助加密请求),因此提供对它的外部引用就足够了。
Kerberos 身份验证
当客户端和服务位于同一个 Kerberos 信任域中时(例如,它们加入同一个 Windows 域),可以使用 Kerberos 身份验证。因为不需要将证书分发给所有参与者,所以它需要的部署工作没有 WSE 支持的任何基于证书的身份验证断言多。此外,Kerberos 基于对称加密法,该方法比公钥加密法速度快。WSE 提供 KerberosAssertion 以帮助您确保使用 Kerberos 票证的消息交换的安全。
KerberosAssertion 不需要在服务端进行任何配置。在客户端上,需要指定目标服务的服务主要名称(Service Principal Name,SPN)。客户端使用 SPN 获得目标服务的 Kerberos 票证。您可以在 XML 策略文件中指定 SPN,如下所示:
<policies> <policy name="HelloWorldPolicy"> <kerberosSecurity> <token> <kerberos targetPrincipal="HOST/myServer"/> </token> </kerberosSecurity> </policy> </policies>
或者,您可以使用 SetClientCredential 方法在代码中为代理直接提供 KerberosToken:
HelloWorld HelloWorld = new HelloWorld(); HelloWorld.SetClientCredential( new KerberosToken("HOST/myServer"));
SetClientCredential 方法和 Kerberos 令牌一起使用时有一个限制:为了防止重放攻击,Windows 操作系统不允许多次使用给定的 Kerberos 票证实例。这意味着每次调用前,您都必须使用前面所示的 SetClientCredential 方法在代理上设置一个新的 KerberosToken 实例。或者,如果您在代码中指定整个策略,则可以直接在 KerberosAssertion 上设置 Kerberos 令牌提供程序。该令牌提供程序充当 KerberosToken 工厂。每当 WSE 运行库需要保护新请求的安全时,它就会调入该提供程序以请求一个新的 KerberosToken 实例。下面显示如何编写代码来设置 Kerberos 令牌提供程序:
KerberosAssertion assertion = new KerberosAssertion(); assertion.KerberosTokenProvider = new KerberosTokenProvider("HOST/myServer"); HelloWorld HelloWorld = new HelloWorld(); HelloWorld.SetPolicy(new Policy(assertion));
图 11 显示如何用 KerberosAssertion 保护消息交换的安全。
图 11 用 KerberosAssertion 保护的消息
添加重放检测
WSE 中的安全断言支持完整性、保密性,以及客户端和服务身份验证。但是,这些断言并不能缓解重放攻击,攻击者截获有效客户端发出的可接受消息,然后,在晚些时候将该消息的副本发送给服务。通常,可以通过重放检测缓解这样的攻击:接收方可以检测并拒绝重复的消息。通过构建在策略框架的可扩展性功能上,重放检测功能可以添加到 WSE 中。
首先,您需要作出一些设计决定。您打算如何确定消息的唯一性?显然,您需要比较传入消息的某些部分。SOAP 信封的部分(您已经在消息之间确保了其唯一性)应该由消息的验证发送方签名;否则,攻击者通过修改消息可以避开重放比较检查,未经检测就传送到接收方。理想情况下,您能够比较发送方签名的所有部分。然而这并不切实际,因为 XML 比较计算上很复杂,而且您需要存储传入消息的实际部分以防止将来的重放攻击。
然而,如果您可以假设所关心的部分是用 WS-Security 规范所要求的XML 数字签名进行数字签名的,则您拥有 SOAP 信封一个非常简洁的部分,它携带的信息熵(或随机性)足够进行重放检测:签名值本身。
使用签名值有三个好处:它携带了足够的信息熵来表示接收方关心的消息部分,对它进行比较既便宜又容易,并且容量小易于存储。为此,重放检测机制的一个理想实现将利用签名值。然而,由于我要突出 WSE 策略框架的可扩展性功能,我将采取一条更为简单的路线,即使用 MessageId,它是一个 WS-Addressing SOAP 标头,推荐使用它携带全局唯一的统一资源标识符(Uniform Resource Identifier,URI)来表示消息。每个自重服务都需要对该标头进行签名,因此,该标头对于消息签名值的信息熵很有帮助。
重放检测机制需要存储以前消息的 MessageId 标头值,以便将它们与新传入的消息进行比较。显然,该过程将导致内存分配不断增加。通过引入对内存消耗和重放检测机制所提供保证的控制,您可以解决该问题。假设您想要限制重放检测机制所能记住的以前消息的最大数量,这样就为内存分配加了一个上限。同时,您可以引入对最低服务质量的控制,在本例中意味着保证能够检测到重放消息的最小时间范围。最后,如果新消息在内存消耗达到极限时到达,您必须考虑系统所需的行为。要同时实现服务质量保证以及内存配额,您只能选择拒绝该消息。重放检测机制的核心是由自定义的 SOAPFilter(请参见图 12)实现的。
ReplayDetectionSOAPFilter 构造函数假定能够保证检测到 MessageId 标头值的最大高速缓存大小以及重放的最小持续时间的默认值。跟踪 MessageId 值所用的数据结构是一个字典,它将 MessageId 值映射到最后一次接收到具有该值的消息的时间。
实现的核心位于 ProcessMessage 方法中。首先,它从传入的消息中提取 MessageId 值。然后,调用 EnsureMessageIdentifierUniqueness 方法,以确保另一个具有相同 MessageId 的消息尚未在最小重放检测时间范围内注册。如果新消息的 MessageId 值碰巧与高速缓存中已存在的 MessageId 值匹配,则代码假定它是一个重放消息,通过在 EnsureMessageIdentifierUniqueness 方法中引发适当的异常来拒绝它。否则,调用 AddMessageIdentifierToCache 将 MessageId 值与高速缓存中的当前时间一起存储。
如果高速缓存大小已经达到其配额,应用程序会首先尝试清除高速缓存,方法是删除最小重放检测时间范围所确定的、不是维护服务的质量所必需的所有项目。如果尝试清除之后高速缓存仍然是满的,代码将引发异常,这是因为它无法同时确保服务质量并保持内存消耗不超过配额。
请密切关注高速缓存上操作周围的锁定,这种情况出现在 ProcessMessage 方法中。必须将 SOAPFilter 上的 ProcessMessage 方法设计成线程安全的,因为可能会在多个线程内调用它以处理不同的消息。
下一步是开发一个策略断言,该断言将创建 ReplayDetectionSOAPFilter,并允许将该功能添加到策略中。图 13 显示 ReplayDetectionPolicyAssertion 的最小实现。请注意,返回 ReplayDetectionSOAPFilter 只是为了处理服务上的传入消息。理论上讲,在处理客户端的传入消息时,重放检测也起了很大作用,但为使该示例简单起见,我决定只在服务端提供该功能。
现在可以通过命令的方式使用 ReplayDetectionPolicyAssertion下面显示如何将该断言添加到策略并将其安装在服务上:
class MyPolicy : Policy { public MyPolicy() : base() { this.Assertions.Add(new KerberosAssertion()); this.Assertions.Add(new ReplayDetectionPolicyAsertion()); } } [WebService] [Policy(typeof(MyPolicy))] public class HelloWorld : WebService { ... }
请注意,在 KerberosAssertion 之后 ReplayDetectionPolicyAssertion 是如何作为 MyPolicy 中的第二个断言安装的。如果您回头看看图 1,就会明白这种顺序将导致先执行 ReplayDetectionSOAPFilter,然后再执行处理服务上的传入消息时 KerberosAssertion 创建的筛选器。在这种情况下,它的作用非常明显:如果消息可能要重放,则您希望在投入时间和财力验证安全性之前拒绝它。
在 XML 文件中指定策略时,要使 ReplayDetectionPolicyAssertion 成为声明性可用的文件,您需要添加序列化功能。PolicyAssertion 定义了一个三方法约定,每个可序列化的策略断言都需要遵守该约定。这三个方法是 ReadXML、WriteXML 和 GetExtensions。
public class ReplayDetectionPolicyAssertion : PolicyAssertion { public override IEnumerable<KeyValuePair<string, Type>> GetExtensions() { return new KeyValuePair<string, Type>[] { new KeyValuePair<string, Type>( "replayDetection", typeof(ReplayDetectionPolicyAssertion)) }; } public override void WriteXml(XmlWriter writer) { if (writer == null) throw new ArgumentNullException("writer"); writer.WriteStartElement("replayDetection"); writer.WriteEndElement(); } public override void ReadXml(XmlReader reader, IDictionary<string, Type> extensions) { if (reader == null) throw new ArgumentNullException("reader"); bool isEmpty = reader.IsEmptyElement; reader.ReadStartElement("replayDetection"); if (!isEmpty) reader.ReadEndElement(); } ... }
ReadXML 方法负责从 XML 策略文件读取策略断言,WriteXML 方法负责将策略断言写入 XML 策略文件。扩展的概念用于在 XML 策略文件中的本地元素名称和代表该元素的 CLR 类型之间提供映射。当 XML 文件中指定的某个策略包含自定义断言(如 ReplayDetectionPolicyAssertion)时,必须在文件开始处在特殊的 extensions 部分内将该断言类型声明为策略的扩展,如下所示:
<policies> <extensions> <extension name="replayDetection" type="WSE3ReplayDetection.ReplayDetectionPolicyAssertion, WSE3ReplayDetection" /> </extensions> <policy name="HelloWorldPolicy"> <kerberosSecurity/> <replayDetection/> </policy> </policies>
extensions 部分可以包含许多扩展元素,每个元素定义一个本地元素名称,并将其与一个完全限定的 CLR 类型名称相关联。只要在需要断言的情况下,当策略分析器遇到 extensions 部分中声明的名称之一时,都会创建一个关联的 CLR 类型的实例并调用它的 ReadXML 方法。
当您的自定义断言被设计成公开一个可扩展性点时,还可以利用 extensions 部分。本文介绍的 ReplayDetectionPolicyAssertion 的一个缺点是,代表以前消息的 MessageId 值的高速缓存保存在内存中。当以任何其他方式循环应用程序域或新建管线实例时,该状态丢失。将重放检测状态保存在内存中还不允许您检测服务器场节点上的重放。
解决该问题的一个方法是使重放检测的实现高速缓存一个可扩展性点。在需要持久性的情况下,实现可以使用数据库而非内存。一旦您拥有了一个 CLR 类型,该类型定义了这样一个可扩展点的协定,您就需要在 ReplayDetectionPolicyAssertion 上公开该类型的一个属性,并且向其参数(如数据库连接字符串)提供序列化和反序列化代码。断言反序列化代码只要求类遵循该约定,并不关心其配置细节。扩展机制可用于注册特定的本地元素名和实现该约定的 CLR 类型之间的映射,从而为捕获 XML 策略文件中的可扩展点提供了一种统一的机制。我已经提供了可扩展 ReplayDetectionPolicyAssertion 的完整实现,以及本文可下载的代码。
小结
您在本文中已经看到,WSE 3.0 中的策略框架提供了一种通用机制,用于转换发送到和发送自 Web 服务的 SOAP 消息。WSE 自己实现的对策略框架的扩展支持各种安全方案。使用 WSE 3.0 中包括的安全策略断言实现可以帮助您创建应用程序,这些应用程序将与使用 Windows Communication Foundation 创建的服务进行互操作。策略框架是可扩展的,并且允许在处理 SOAP 消息时实现自定义功能,同时利用命令性和声明性策略规范。
原文地址:http://www.microsoft.com/china/MSDN/library/WebServices/WebServices/issues0602WSE30.mspx?mfr=true