首页 > 解决方案 > 如何在 C# 中为 SOAP 请求正确生成安全标头?

问题描述

我正在尝试使用 C# 使用 SOAP 服务,但我正在努力处理 Outgoing WS-Security 配置。

到目前为止我所做的:

  1. 在 VS 2019 中创建了一个服务引用并扩展了生成的 Client 类以在其中传递我的配置
  2. 创建了一个自定义端点行为:

    this.ChannelFactory.Endpoint.EndpointBehaviors.Add(new WsSecurityEndpointBehavior(username, password, timeout));
    // ...

    public class WsSecurityEndpointBehavior : IEndpointBehavior
    {
        private readonly string _password;
        private readonly TimeSpan _timeout;
        private readonly string _username;
    
        public WsSecurityEndpointBehavior(string username, string password, TimeSpan timeout)
        {
            this._username = username;
            this._password = password;
            this._timeout = timeout;
        }
    
        void IEndpointBehavior.AddBindingParameters(ServiceEndpoint endpoint,
            BindingParameterCollection bindingParameters)
        {
        }
    
        void IEndpointBehavior.ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
        {
            clientRuntime.ClientMessageInspectors.Add(new WsSecurityMessageInspector(_username, _password, _timeout));
        }
        void IEndpointBehavior.ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
        {
        }
    
        void IEndpointBehavior.Validate(ServiceEndpoint endpoint)
        {
        }
    }

  1. 创建了一个 ClientMessageInspector:

    public class WsSecurityMessageInspector : IClientMessageInspector
    {
        private readonly string _password;
        private readonly TimeSpan _timeout;
        private readonly string _username;
    
        public WsSecurityMessageInspector(string username, string password, TimeSpan timeout)
        {
            this._username = username;
            this._password = password;
            this._timeout = timeout;
        }
    
        object IClientMessageInspector.BeforeSendRequest(ref Message request, IClientChannel channel)
        {
            var header = new Security
            {
                UsernameToken =
                {
                    Password = new Password
                    {
                        Value = _password,
                        Type =
                            "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest"
                    },
                    Username = _username
                },
                Timestamp =
                {
                    Created = DateTime.UtcNow.ToString("yyyy-MM-ddThh:mm:ss.fffZ"),
                    Expires = DateTime.UtcNow.Add(_timeout).ToString("yyyy-MM-ddThh:mm:ss.fffZ")
                }
            };
            request.Headers.Add(header);
            return null;
        }
    
        void IClientMessageInspector.AfterReceiveReply(ref Message reply, object correlationState)
        {
        }

}
  1. 为 xml 字段添加了 poco 类:

    public class Password
    {
        [XmlAttribute] public string Type { get; set; }
        [XmlText] public string Value { get; set; }
    }
    
    public class UsernameToken
    {
        [XmlElement] public string Username { get; set; }
        [XmlElement] public Password Password { get; set; }
    }
    
    public class Timestamp
    {
        [XmlElement] public string Created { get; set; }
        [XmlElement] public string Expires { get; set; }
    }

  1. 最后实现了一个安全消息头:

    public class Security : MessageHeader
    {
        public Security()
        {
            UsernameToken = new UsernameToken();
            Timestamp = new Timestamp();
        }
    
        public UsernameToken UsernameToken { get; set; }
        public Timestamp Timestamp { get; set; }
    
        public const string SoapEnvelopeNamespace = "http://schemas.xmlsoap.org/soap/envelope/";
        public const string WsseUtilityNamespaceUrl = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd";
        public const string WsseNamespace = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd";
    
    
        public override string Name => GetType().Name;
    
        public override string Namespace => WsseNamespace;
    
        public override bool MustUnderstand => true;
    
        protected override void OnWriteStartHeader(XmlDictionaryWriter writer, MessageVersion messageVersion)
        {
            writer.WriteStartElement("wsse", Name, Namespace);
            //writer.WriteAttributeString("s", "mustUnderstand", SoapEnvelopeNamespace, "1"); // - doesn't work either way
    
            writer.WriteXmlnsAttribute("wsse", Namespace);
            writer.WriteXmlnsAttribute("wsu", WsseUtilityNamespaceUrl);
        }
    
        protected override void OnWriteHeaderContents(XmlDictionaryWriter writer, MessageVersion messageVersion)
        {
            // Timestamp
            writer.WriteStartElement("wsu", "Timestamp", WsseUtilityNamespaceUrl);
    
            writer.WriteAttributeString("wsu", "Id", WsseUtilityNamespaceUrl, "TS-" + Guid.NewGuid());
    
            writer.WriteStartElement("wsu", "Created", WsseUtilityNamespaceUrl);
            writer.WriteValue(Timestamp.Created);
            writer.WriteEndElement();
    
            writer.WriteStartElement("wsu", "Expires", WsseUtilityNamespaceUrl);
            writer.WriteValue(Timestamp.Expires);
            writer.WriteEndElement();
    
            writer.WriteEndElement(); // Timestamp
    
            // username token
            writer.WriteStartElement("wsse", "UsernameToken", Namespace);
            writer.WriteAttributeString("wsu", "Id", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd", "UsernameToken-" + Guid.NewGuid());
            // Username
            writer.WriteStartElement("wsse", "Username", Namespace);
            writer.WriteValue(UsernameToken.Username);
            writer.WriteEndElement();
    
            var nonce = Convert.ToBase64String(Encoding.UTF8.GetBytes(Guid.NewGuid().ToString()));
            var createdStr = Timestamp.Created;
            var pwd = GetSHA1String(nonce + createdStr + UsernameToken.Password.Value);
    
            // Password
            writer.WriteStartElement("wsse", "Password", Namespace);
            writer.WriteAttributeString("Type", UsernameToken.Password.Type); //PasswordDigest
            writer.WriteValue(pwd);
            writer.WriteEndElement();
    
            writer.WriteStartElement("wsse", "Nonce", Namespace);
            writer.WriteAttributeString("EncodingType", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary");
            writer.WriteValue(nonce);
            writer.WriteEndElement();
    
            writer.WriteStartElement("wsu", "Created", WsseUtilityNamespaceUrl);
            writer.WriteValue(createdStr);
            writer.WriteEndElement();
    
            // username token
            writer.WriteEndElement();
        }
    
        private static string GetSHA1String(string phrase)
        {
            var sha1Hasher = new SHA1CryptoServiceProvider();
            var hashedDataBytes = sha1Hasher.ComputeHash(Encoding.UTF8.GetBytes(phrase));
            return Convert.ToBase64String(hashedDataBytes);
        }
    }

我在结果中的 XML 示例:


    <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">
        <wsu:Timestamp wsu:Id="TS-2b7092f4-8f62-40b5-8f7e-ab774ef967aa">
            <wsu:Created>2021-05-17T10:20:16.913Z</wsu:Created>
            <wsu:Expires>2021-05-17T10:25:16.913Z</wsu:Expires>
        </wsu:Timestamp>
        <wsse:UsernameToken wsu:Id="UsernameToken-8ee97b0e-cbdf-465b-97b0-8e5194f2f4a9">
            <wsse:Username>username</wsse:Username>
            <wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest">base64_described_above</wsse:Password>
            <wsse:Nonce EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary">MGNiNDUxMTYtYjBlMy00ZmQzLTgwYTUtODgxZGY2NmUyMDI0</wsse:Nonce>
            <wsu:Created>2021-05-17T10:20:16.913Z</wsu:Created>
        </wsse:UsernameToken>
    </wsse:Security>

我能够让它在 Soap UI 中工作,但是通过这个 C# 实现,我得到了ns1:SecurityError. 我究竟做错了什么?

提前致谢。

----更新---- 完整的请求 XML:


    <?xml version="1.0" encoding="utf-16"?>
    <s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
        <s:Header>
            <Action s:mustUnderstand="1"
                xmlns="http://schemas.microsoft.com/ws/2005/05/addressing/none">[ActionICall]</Action>
            <h:RequestContext xmlns="http://hidden-for-security/webservices/v1/messages"
                xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                xmlns:xsd="http://www.w3.org/2001/XMLSchema"
                xmlns:h="http://hidden-for-security/webservices/v1/messages">
                <Id>f7ed31dc-b7ad-4a5d-ae17-c86663bbb2da</Id>
                <Language>en</Language>
            </h:RequestContext>
            <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">
                <wsu:Timestamp wsu:Id="TS-ceb077e4-3255-405b-aac7-d7ae9bf1b884">
                    <wsu:Created>2021-05-17T12:01:11.852Z</wsu:Created>
                    <wsu:Expires>2021-05-17T12:06:11.852Z</wsu:Expires>
                </wsu:Timestamp>
                <wsse:UsernameToken wsu:Id="UsernameToken-1373fa47-6f2a-4d44-87d5-5270132f9830">
                    <wsse:Username>username</wsse:Username>
                    <wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest">calculated-password-in-base64</wsse:Password>
                    <wsse:Nonce EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary">MjkxY2VkY2MtZjA4ZC00NmQ0LWFkMjUtYWQ4ZGU3ODFmNGVm</wsse:Nonce>
                    <wsu:Created>2021-05-17T12:01:11.852Z</wsu:Created>
                </wsse:UsernameToken>
            </wsse:Security>
        </s:Header>
        <s:Body xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
            xmlns:xsd="http://www.w3.org/2001/XMLSchema">
            <ActionICallRequest xmlns="http://hidden-for-security/webservices/v1/messages">
                <UniqueNumber xmlns="http://hidden-for-security/webservices/v1/datamodel">659648795</UniqueNumber>
            </ActionICallRequest>
        </s:Body>
    </s:Envelope>

我得到的回应:


    <?xml version="1.0" encoding="utf-16"?>
    <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
      <s:Header xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" />
      <soap:Body>
        <soap:Fault>
          <faultcode xmlns:ns1="http://ws.apache.org/wss4j">ns1:SecurityError</faultcode>
          <faultstring>A security error was encountered when verifying the message</faultstring>
        </soap:Fault>
      </soap:Body>
    </soap:Envelope>

标签: c#soapws-security

解决方案


推荐阅读