You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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头,完全可控不会出现格式兼容问题:

  1. 首先创建自定义消息检查器类,实现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;
    }
}
  1. 创建自定义终结点行为,把检查器绑定到客户端:
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)
    {
    }
}
  1. 调用服务时绑定自定义行为即可,注意要把原来的安全模式设置为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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.07 15:18:00