(万恶的微软已在KB971831/KB976462添加了一个TransportSecurityBindingElement.AllowInsecureTransport来实现本文的功能了. 日啊~)
以前我们部门一直通过WSE生成代理类来访问JAVA部门生成的WebService, 因为最近部门里把Visual Studio都升级到2008版了, 大家发现WSE不能再在VS2008里生成其代理类了. 上GOOGLE上调查了一翻, 原来微软为了推广WCF, 不再升级WSE了. 被迫开始研究WCF @_@ 啊~~~~
同时微软提供了svcutil.exe工具(万恶的命令行程序)帮助开发人员生成访问WebService要用的Client类, 于是网上有好事者做了个svcutil.exe的GUI程序. 我本以为这样生成了Client类, 就能直接访问. 可是一测试问题马上就出来了. 访问带验证的WebService时, WCF本身提供的一些Binding类要求SSL连接或服务器证书. 而我们之前都是用WSE生成的代理类直接访问普通HTTP通道的WebService, 而支持HTTP通道的BasicHttpBinding又不支持UserName/Password验证.
然而, GOOGLE的过程是痛苦的... 最终得到的N个搜索结果都指向了 Yaron Naveh的ClearUsernameBinding类. 虽然ClearUsernameBinding并没有直接解决我的问题, 但是从其源代码中我明白如何破解WCF内置验证的方法.
要解决我们这个问题的关键在于创建一个以HTTP为通道并支持UserName/Password验证的Binding类. WCF提供了一个CustomBinding类, 按照MSDN上一篇名为"Creating User-Defined Bindings"的说法, 我们需要给CustomBinding类提供一些BindingElement. 针对当前的问题, 我们需要生成TextMessageEncodingBindingElement, HttpTransportBindingElement和TransportSecurityBindingElement (使用SecurityBindingElement.CreateUserNameOverTransportBindingElement创建).
1. 首先将TextMessageEncodingBindingElement.MessageVersion设置为Soap11 (默认为Soap12WSAddressing10, 而我们JAVA部门生成的WebService是Soap1.1的).
2. 然后将TransportSecurityBindingElement.IncludeTimestamp设置为false. 就是因为 ClearUsernameBinding 没有这一步所以它无法解决我们的问题. 其根源可能是JAVA服务端不支持Timestamp吧.
3. 直接使用 HttpTransportBindingElement 是无法通过WCF本身的验证的, 根据错误信息, 我反射到了 System.ServiceModel.Dispatcher.SecurityValidationBehavior.SoapOverSecureTransportRequirementsRule.ValidateSecurityBinding 这个方法的源代码. 很容易看出我们需要修改 HttpTransportBindingElement.GetProperty 方法的返回的 ISecurityCapabilities 实例的一些值才能通过 ValidateSecurityBinding 方法的验证. 因为ISecurityCapabilities的唯一实现类是internal的, 而且另外2个BindingElement都是sealed的, 所以我们只能自己实现一个 ISecurityCapabilities, 然后再派生一个 HttpTransportBindingElement来重写其 GetProperty 方法来返回我们实现的 ISecurityCapabilities类. 于是我定义了一个 NonSecuredHttpTransportBindingElement 类来继承 HttpTransportBindingElement, 并重写其 GetProperty 方法用来返回我们需要的 ISecurityCapabilities 实体.
public class NonSecuredHttpTransportBindingElement : HttpTransportBindingElement{public NonSecuredHttpTransportBindingElement() : base() { }public NonSecuredHttpTransportBindingElement(NonSecuredHttpTransportBindingElement elementToBeCloned) : base(elementToBeCloned) { }public override T GetProperty<T>(BindingContext context){if(typeof(T) == typeof(ISecurityCapabilities)){return (T)(ISecurityCapabilities)(new NonSecuredHttpSecurityCapabilities());}return base.GetProperty<T>(context);}public override BindingElement Clone(){return new NonSecuredHttpTransportBindingElement(this);}class NonSecuredHttpSecurityCapabilities : ISecurityCapabilities
{#region ISecurityCapabilities Members
public ProtectionLevel SupportedRequestProtectionLevel { get { return ProtectionLevel.EncryptAndSign; } }public ProtectionLevel SupportedResponseProtectionLevel { get { return ProtectionLevel.EncryptAndSign; } }public bool SupportsClientAuthentication { get { return false; } }public bool SupportsClientWindowsIdentity { get { return false; } }public bool SupportsServerAuthentication { get { return true; } }#endregion
}}
现在可以试着访问JAVA部门提供的WebService了. 首先使用svcutil.exe生成一个WCF Client类, 然后写入下列测试代码.CustomBinding binding = new CustomBinding();
TransportSecurityBindingElement el1 = SecurityBindingElement.CreateUserNameOverTransportBindingElement();el1.IncludeTimestamp = false;
binding.Elements.Add(el1);TextMessageEncodingBindingElement el2 = new TextMessageEncodingBindingElement();
el2.MessageVersion = MessageVersion.Soap11;binding.Elements.Add(el2);NonSecuredHttpTransportBindingElement el3 = new NonSecuredHttpTransportBindingElement();
binding.Elements.Add(el3);EndpointAddress ea = new EndpointAddress("http://javadepartment:7777/gateway/services/SID0000010?wsdl");SearchServiceEJBWebServiceClient client = new SearchServiceEJBWebServiceClient(binding, ea);
client.ClientCredentials.UserName.UserName = "[UserName]";
client.ClientCredentials.UserName.Password = "[Password]";
string result = client.querybuilder("Taylor Swift", "Search All");
终于拿到了久违的正确结果, 从无尽的Exception中解脱出来了~~!!
参考文档: Introducing WCF ClearUsernameBinding, Creating User-Defined Bindings