VS2017中为WCF服务SOAP请求添加WSSE安全头失败求解决方案
问题描述
我需要在发往服务的SOAP请求中添加如下WSSE XML数据:
<soapenv:Header> <wsse:Security xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"> <wsse:UsernameToken wsu:Id="UsernameToken-D67150EFEFE71BA23416294396650191"> <wsse:Username>XXXXXXX</wsse:Username> <wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText">XXXXXXX</wsse:Password> <wsse:Nonce EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary">XXXXXXX</wsse:Nonce> <wsu:Created>2021-08-20T06:07:45.015Z</wsu:Created> </wsse:UsernameToken> </wsse:Security> </soapenv:Header>
我已经在VS 2017中将该服务添加为服务引用,查到的方案包括在app.config中进行如下配置:
<bindings> <basicHttpBinding> <binding name="myBinding"> <security mode="TransportWithMessageCredential" > <message clientCredentialType="UserName" /> </security> </binding> </basicHttpBinding> </bindings> <client> <endpoint address="https://...." binding="basicHttpBinding" bindingConfiguration="myBinding" contract="..." name="..." > <headers> <wsse:Security xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"> <wsse:UsernameToken> <wsse:Username>XXX</wsse:Username> <wsse:Password Type='http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText'>XXX</wsse:Password> </wsse:UsernameToken> </wsse:Security> </headers> </endpoint> </client>
还有使用BasicHttpBinding的代码方案,示例如下:
var binding = new BasicHttpBinding(BasicHttpSecurityMode.TransportWithMessageCredential); binding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Basic; binding.Security.Message.ClientCredentialType = BasicHttpMessageCredentialType.UserName; var endpoint = new EndpointAddress("https://...."); using (var client = new ServiceReference1.Client(binding, endpoint)) { client.ClientCredentials.UserName.UserName = "XYZ"; client.ClientCredentials.UserName.Password = "XYZ"; ServiceReference1.Request request = new ServiceReference1.Request(); request.Request = new ServiceReference1.RequestType(); request.Request.Nr = "12345"; ServiceReference1.Response response = client.getData(request); ServiceReference1.ResponseType[] responseMessages = response.Response; }
但我尝试以上两种方案都没有成功,所用的用户名和密码已经在SoapUI中测试可用,请问有什么可行的解决建议?
解决建议
你尝试的方案失败核心原因是WCF原生的BasicHttpBinding开启用户名验证时,默认不会生成WSSE要求的Nonce和Created字段,而你的目标服务端强制要求这两个参数,SoapUI调用时会自动生成这两个参数所以能正常调用。以下是三种可行的解决方案:
方案1:自定义消息拦截器(最稳定,推荐生产环境使用)
通过WCF的客户端消息拦截器手动构造完全符合要求的WSSE头,完全可控不会出现格式兼容问题:
- 首先创建自定义消息检查器类,实现
IClientMessageInspector接口:
using System; using System.ServiceModel; using System.ServiceModel.Channels; using System.ServiceModel.Dispatcher; using System.Xml; public class WsseHeaderInspector : IClientMessageInspector { private readonly string _username; private readonly string _password; public WsseHeaderInspector(string username, string password) { _username = username; _password = password; } public void AfterReceiveReply(ref Message reply, object correlationState) { // 不需要处理返回值可以留空 } public object BeforeSendRequest(ref Message request, IClientChannel channel) { // 生成Nonce和时间戳 var nonce = Guid.NewGuid().ToByteArray(); var created = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"); // 创建WSSE头 var doc = new XmlDocument(); var securityElement = doc.CreateElement("wsse", "Security", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"); var usernameTokenElement = doc.CreateElement("wsse", "UsernameToken", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"); // 用户名节点 var usernameElement = doc.CreateElement("wsse", "Username", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"); usernameElement.InnerText = _username; usernameTokenElement.AppendChild(usernameElement); // 密码节点 var passwordElement = doc.CreateElement("wsse", "Password", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"); passwordElement.SetAttribute("Type", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText"); passwordElement.InnerText = _password; usernameTokenElement.AppendChild(passwordElement); // Nonce节点 var nonceElement = doc.CreateElement("wsse", "Nonce", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"); nonceElement.SetAttribute("EncodingType", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary"); nonceElement.InnerText = Convert.ToBase64String(nonce); usernameTokenElement.AppendChild(nonceElement); // Created节点 var createdElement = doc.CreateElement("wsu", "Created", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"); createdElement.InnerText = created; usernameTokenElement.AppendChild(createdElement); securityElement.AppendChild(usernameTokenElement); // 把头注入到请求中 var ms = new System.IO.MemoryStream(); var writer = XmlWriter.Create(ms); securityElement.WriteTo(writer); writer.Flush(); ms.Position = 0; var reader = XmlReader.Create(ms); var header = MessageHeader.CreateHeader("Security", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd", reader.ReadElementContentAsXElement(), false); request.Headers.Add(header); return null; } }
- 创建自定义终结点行为,把检查器绑定到客户端:
using System.ServiceModel.Description; using System.ServiceModel.Dispatcher; public class WsseHeaderBehavior : IEndpointBehavior { private readonly string _username; private readonly string _password; public WsseHeaderBehavior(string username, string password) { _username = username; _password = password; } public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters) { } public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime) { clientRuntime.ClientMessageInspectors.Add(new WsseHeaderInspector(_username, _password)); } public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher) { } public void Validate(ServiceEndpoint endpoint) { } }
- 调用服务时绑定自定义行为即可,注意要把原来的安全模式设置为
None,避免WCF自动生成多余的安全头:
var binding = new BasicHttpBinding(BasicHttpSecurityMode.Transport); // 如果是http协议就改为None var endpoint = new EndpointAddress("https://你的服务地址"); using (var client = new ServiceReference1.Client(binding, endpoint)) { // 绑定自定义WSSE头行为 client.Endpoint.EndpointBehaviors.Add(new WsseHeaderBehavior("你的用户名", "你的密码")); // 正常构造请求调用即可 var request = new ServiceReference1.Request { Request = new ServiceReference1.RequestType { Nr = "12345" } }; var response = client.getData(request); }
方案2:配置文件自定义绑定(无需改代码,适合快速验证)
如果你不想写自定义拦截器,也可以通过自定义绑定配置强制WCF生成Nonce和Created字段,修改app.config如下:
<bindings> <customBinding> <binding name="WsseBinding"> <security authenticationMode="UserNameOverTransport" includeTimestamp="true"> <secureConversationBootstrap /> </security> <textMessageEncoding messageVersion="Soap11" /> <httpsTransport /> <!-- 如果是http协议就改为httpTransport --> </binding> </customBinding> </bindings> <client> <endpoint address="https://你的服务地址" binding="customBinding" bindingConfiguration="WsseBinding" contract="你的服务契约" name="你的终结点名" /> </client>
调用时直接用原来的代码设置用户名密码即可:
using (var client = new ServiceReference1.Client()) { client.ClientCredentials.UserName.UserName = "你的用户名"; client.ClientCredentials.UserName.Password = "你的密码"; // 调用对应方法 }
排查建议
- 调用前用Fiddler抓包,对比SoapUI生成的请求和你代码生成的请求,重点检查
Security节点的命名空间、Password的Type属性、Nonce的编码格式是否完全一致 - 注意
Created字段必须是UTC时间,和服务端时间偏差超过5分钟大多数服务端会直接拒绝请求 Nonce必须是每次请求唯一的随机值,不能重复使用
内容的提问来源于stack exchange,提问作者K.M.Rasmussen
相关产品推荐
相关产品推荐

