安全问题
WCF提供了通道安全和消息安全两种方式保证消息安全,为了保证服务的通用性,WCF提供的安全方案都基于公共标准的安全规范,比如ITU-T的X.509等。遵守公共安全规范可以保证WCF的通用性,不过有时候这些通用安全方式并不适合我们。在一些简单的场合,我们仅仅就是想对消息加一下密,使用X.509证书就显得过重了。
如果要弄证书,合法证书当然是最好的了。但弄个合法X.509证书是需要费用的,而且每年都要交费,很少客户会乐意。弄个非法的X509证书凑合用也是问题多多,首先证书需要在服务器上安装,还需要找个妥当的方式为客户端分发,证书的安装会带来额外的配置工作。就算安装配置都不是问题,在silverlight中使用假证书也会很麻烦,在普通.net客户端中,至少还能有System.Net.ServicePointManager.ServerCertificateValidationCallback属性可以让你选择性的去信任自己创建的假证书,但silverlight就不提供这个机制,它太安全了,现在安全得让人头疼。
为了避开证书实现silverlight可用的WCF消息加密,看来要自已动手了。WCF提供了丰富的扩展机制(参考:http://msdn.microsoft.com/zh-cn/library/ms733848),通过自定义消息的编码器(参考:http://msdn.microsoft.com/zh-cn/library/ms751486),在消息编码解码的过程中加入自己的加密解密运算可以达到目的。注意这个做法将破坏服务的通用性,你无法公开这样的服务给大众,因为他们不了解你的服务消息格式,不过在这里,我们的服务是为我们自己的客户端服务的,我们反倒期望这样的效果。
决定了努力的方向,下一步就是设计一个自己的加解密算法了,这个听住似乎不难,其实并不是很简单,由于处理目标是通过网络传输的消息,下面几件事情就需要考虑:
1.机密性。除了发送者和接收者外,别人应看不懂消息。
2.完整性。如果消息传输过程中被别人篡改过,要能识别并拒绝该消息。
3.抗重放。如果消息被别人原样记下并重新发送,要能识别并拒绝该消息。
4.身份认证。传输的消息到底是谁发送出去的。
5.抗抵赖。消息发送者不能否认自己发送过某个消息。
6.乱序。消息间的顺序被别人重新排列或消息发送时机被变更。
7.行为分析。分析消息的大小,时间段,目标及频率等特征,以判断并得知消息代表的意义。
8.拒绝服务攻击。发送大量恶意消息,使服务器或网络不堪重负,或直接破坏服务器或网络设施。
由于不是银行、军事等重要场景,这里不考虑拒绝服务、行为分析和乱序问题,身份认证和抗抵赖也可以放松。但机密性、完整性、抗重放是必须要保证,如果连这三条都保证不了,可以认为这套加密机制根本就不可用。
还有一个额外问题,那就是自定的编码器要能通过配置来处理过大的数据,在实际应用时,WCF数据超过默认限制大小的事情并不少见。
加密内容就可以解决机密性问题,这里使用普通的对称密钥加密,使用系统用户密码的哈希值做为密钥,因为只有用户自己和服务器知道这个哈希值,所以不用把密钥在网络上传输,降低了密钥泄露风险。
使用内容摘要做为签名可以解决完整性问题,这里使用键控哈希算法计算摘要,在一定程序上保证签名不会被其它用户伪造。
建立了时间窗口,不在时间窗口范围的消息不处理,在时间窗口范围的消息做记录,根据记录检查出的重复消息也不处理。这样可以解决重放问题。
具体算法流程在本文的示例中有文档,这里只谈谈如何复用本文示例。
本文的示例和微软给出的示例有些不同。微软的示例把编码解码逻辑封装了一个程序集中,可供服务端和客户端共同引用,但本文的的客户端是silverlight,它无法引用普通的.net类库,所以没有封装dll,分别在服务端和客户端写代码。这个方法适应性较文,在非silverlight的普通场合也可以使用。
在服务端安装自定义编码器:
1.把本文示例服务端的CustomTextEncoder.cs、CustomTextEncoderBindingElement.cs、CustomTextEncoderBindingElementSection.cs、CustomTextEncoderFactory.cs复制到程序中,在程序配置文件中注册自定义的编码器:
<system.serviceModel>
<extensions>
<bindingElementExtensions>
<add name="customTextCodeEncoding" type="SilverlightWcfExample.Web.CustomTextEncoderBindingElementSection, SilverlightWcfExample.Web"/>
</bindingElementExtensions>
</extensions>
</system.serviceModel>
注意根据实际修改命名空间、程序集名等。
2.建立自定义绑定:
<customBinding>
<binding name="CustomTextBinding_MessageSecurityService" closeTimeout="00:10:00" openTimeout="00:10:00" receiveTimeout="00:10:00" sendTimeout="00:10:00" >
<customTextCodeEncoding maxReadPoolSize="64000" maxWritePoolSize="16000">
<readerQuotas maxArrayLength="16384000" maxBytesPerRead="4096000" maxDepth="32000" maxNameTableCharCount="16384000" maxStringContentLength="8192000" />
</customTextCodeEncoding>
<httpTransport maxReceivedMessageSize="65536000" maxBufferSize="65536000" />
</binding>
</customBinding>
本文的自定义编码器支持配置readerQuotas等参数,以支持大数据和长处理时间的服务。
3.修改服务的配置,把binding和bindingConfiguration设为自定义的项。
<service name="SilverlightWcfExample.Web.Services.SqlDataService">
<endpoint address="" binding="customBinding" bindingConfiguration="CustomTextBinding_MessageSecurityService"
contract="SilverlightWcfExample.Web.Services.SqlDataService" />
<endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange" />
</service>
到此服务端改造完成。
在客户端使用自定义编码器,如果是普通.net客户端,可参考微软示例配置。这里介绍silverlight客户端,由于silverlight不支持某些配置,所以使用起来有些不同:
1.把本文示例客户端CustomTextEncoder.cs、CustomTextEncoderBindingElement.cs、CustomTextEncoderFactory.cs复制到程序中,可以发现silverlight不支持CustomTextEncoderBindingElementSection.cs。
2.像引用普通服务那样引用服务端的服务,生成代码和配置项。为了提高服务适应性,可以把openTimeout等值调大些:
<customBinding>
<binding name="CustomBinding_SqlDataService" closeTimeout="00:10:00" openTimeout="00:10:00" receiveTimeout="00:10:00" sendTimeout="00:10:00">
<textMessageEncoding />
<httpTransport maxReceivedMessageSize="2147483647" maxBufferSize="2147483647" />
</binding>
</customBinding>
3.在silverlight生成的配置项里,编码器用的是textMessageEncoding,我们无法通过配置改变它,所以要使用代码在调用服务时替换成自己的编码器。把替换编码器的代码封装到下面这个方法中:
internal static class WcfServiceHelper
{
internal static void ReplaceEndpoint(ServiceEndpoint endpoint)
{
BindingElementCollection bindingCollection = endpoint.Binding.CreateBindingElements();
bindingCollection[0] = new CustomTextEncoderBindingElement();
Binding binding = new CustomBinding(bindingCollection);
endpoint.Binding = binding;
}
}
4.所有服务的引用在调用服务前都需要用上步的方法处理一下:
SqlDataServiceClient client = new SqlDataServiceClient();
WcfServiceHelper.ReplaceEndpoint(client.Endpoint);
client.DoSomethingAsync();
到此,供silverlight使用的WCF的无证书消息加密工作已经完成。上面的方法并不优雅,但起码是有效的。如果大家有更好的方法,请指教,谢谢。