前言
====
在最近的工作中需要对 Web Service 的传输内容进行加密和数字签名,我使用 SOAP 扩展来更改 SOAP 消息的方式来进行加解密,这样就无需对原有的程序进行代码改进就可达成安全传输的目的。
思路
====
我的设计思路是:客户端程序通过配置文件来通知继承自 SoapExtension 的加解密类是否需要加密发送的消息,并在该 Soap 消息中自动新增一个扩展的 Soap 消息头,用其表示该 Soap 消息包是否经过加密。当服务器接受到该 Soap 消息包时,先提取该扩展 Soap 消息头,并根据其指定的加密状态来决定是否需要进行签名验证和解密处理。服务器返回消息时,也将根据获取的客户端的加解密要求来决定是否对反馈信息进行加密和签名。这样,就可以由每个客户端自行根据其安全需要来决定是否需要使用加密处理,服务器将自动根据每个接受的消息进行解密判断,各客户端可独立行使各自独立的安全级别而不影响到其他的使用者。
过程
====
在 SoapExtension 中是通过重写其 ProcessMessage 方法来实现对每次收发 Soap 消息的处理的。在该方法唯一的 SoapMessage 类型的参数中包含一个 Stream 类型的消息流对象,该对象就是收发的 Soap 消息包。
在每次客户端发送消息前(BeforeSerialize 阶段),根据其配置文件指定的标志来决定是否进行加密,并新增一个扩展 SoapHeader 到当前 Soap 包的消息头列表中,该扩展的 SoapHeader 用来指示接受者该 Soap 包是否经过加密和数字签名。在 AfterSerialize 阶段就根据指示对 Soap 消息包进行加密。服务器端,在 BeforeDeserialize 阶段,先获取扩展 SoapHeader 对象,根据其指示来决定对接收到的 Soap 消息包进行解密。
解决
====
该方案忽略了一个事实,那就是 SoapHeader 是处在 Soap 消息包中的,如果对整个 Soap 消息包进行加密,则无法在解密整个 Soap 消息之前先获得 SoapHeader,不过,这并不妨碍该方案的实施。
第一,不要加密整个 Soap 消息包,而只加密 Soap 消息包的主体(Body)部分;
这个解决办法有个不好的地方就是不能对 SoapHeader 进行保密,故此,最好不要用 SoapHeader 来保存和传输机要数据。
第二,还是加密整个 Soap 消息包,只不过是使用自定义的 HttpHeader 来标示当前附带的 Soap 消息包的加密状态。
这种解决方案的不足之处是依赖于特定的传输层协议。当然,也可以自定义一种格式,将指示消息包加密状态的标识放在消息主体中,在接收到该消息包时,先解析其指示值后再进行后续操作。
我使用的是第二种解决方案(即 使用 HTTP 标头来标识 Soap 消息包的加密状态)。这在 SoapExtension 中遇到了个麻烦,因为 SoapExtension 并没有提供给扩展实现者处理本次 Http 会话消息头的机会/方法。在 SoapMessage 中有个 ContentType[String] 属性,用以获取或设置 SOAP 请求或 SOAP 响应的 HTTP Content-Type(默认为 text/xml),噫~ 正好可以用它来表示 Soap 消息包的加密状态,虽然 MSDN 不建议更改它。用 LoadRunner 侦测 Soap 消息,发现其值为“text/xml charset=utf-8;”,将其更改为“text/xml charset=utf-8; isCryptograph=True”后发现整个 Soap 消息的字符编码出现错误,接收者获取的内容完全变成了乱码,不过在 .NET Framework 1.1 中则无该问题,当然,如果是在 .NET Framework 1.1 中建议使用 ContentEncoding 属性来扩展自己的标识,可惜在 1.0 中没有提供该属性 :-(
最后,在 .NET Framework 1.0 的架构中,使用在客户端和服务端的配置文件来指示是否对 Soap 消息包进行加解密,不过,这样就必须保持客户端和服务端对配置标志的一致性。唉~
后记:在客户端和服务端的 BeforeDeserialize 阶段中 SoapMessage.Stream 对象的 CanSeek 属性是不同的(服务端该 CanSeek 为 False),对于这点要小心处理,对此我甚为不解!
相关文章:《安全之道:加密与数字签名》