¿Cómo puedo invocar un Servicio SOAP con WS-Security con WCF en tiempo de ejecución?

publicado por: Anonymous

Tengo el siguiente problema, necesito invocar a un Servicio Web SOAP utilizando el esquema de seguridad WS-Security, pero debido a que son varios endpoint muy parecidos, quiero implementarlo de manera dinámica en tiempo de ejecución, he visto otras soluciones como enviar la trama XML como un string y luego enviarlo a través de HttpWebRequest:

private XmlDocument CallWebService(string method, string operation, string xmlPayload)
{
    string result = "";
    string CREDENTIALS = "PASSword123";
    string URL_ADDRESS = "http://www.client.com/_ws/" + method + "?sso=" + CREDENTIALS + "&o=" + operation +;  //TODO: customize to your needs
    // ===== You shoudn't need to edit the lines below =====

    // Create the web request
    HttpWebRequest request = WebRequest.Create(new Uri(URL_ADDRESS)) as HttpWebRequest;

    // Set type to POST
    request.Method = "POST";
    request.ContentType = "application/xml";

    // Create the data we want to send
    StringBuilder data = new StringBuilder();
    data.Append(xmlPayload);
    byte[] byteData = Encoding.UTF8.GetBytes(data.ToString());      // Create a byte array of the data we want to send
    request.ContentLength = byteData.Length;                        // Set the content length in the request headers

    // Write data to request
    using (Stream postStream = request.GetRequestStream())
    {
        postStream.Write(byteData, 0, byteData.Length);
    }

    // Get response and return it
    XmlDocument xmlResult = new XmlDocument();
    try
    {
        using (HttpWebResponse response = request.GetResponse() as HttpWebResponse)
        {
            StreamReader reader = new StreamReader(response.GetResponseStream());
            result = reader.ReadToEnd();
            reader.Close();
        }
        xmlResult.LoadXml(result);
    }
    catch (Exception e)
    {
        xmlResult = CreateErrorXML(e.Message, "");  //TODO: returns an XML with the error message
    }
    return xmlResult;
}

Pero creo que WCF tiene una solución mejor Y mucho más elegante. Este es el bloque de código XML que debo enviar como Request:

<soapenv:Envelope xmlns:ser="http://service.sunat.gob.pe" xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
   <soapenv:Header>
      <wsse:Security soapenv:mustUnderstand="1" 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="ABC-123">
            <wsse:Username>USERNAME</wsse:Username>
            <wsse:Password>PASSWORD</wsse:Password>
         </wsse:UsernameToken>
      </wsse:Security>
   </soapenv:Header>
   <soapenv:Body>
      <ser:sendBill>
         <fileName>20132263544-20-R001-0005649.zip</fileName>
         <contentFile>UEsDBC0AAAAIAOdhLUhAR3s5</contentFile>
      </ser:sendBill>
   </soapenv:Body>
</soapenv:Envelope>

Si alguien puede ayudarme con el código en C# le estaré eternamente agradecido.
Saludos desde Peru.

solución

Finalmente después de muchas cavilaciones, pude encontrar la solución, fue un poco complejo pero no está demás dar las gracias a @Leandro Tuttini, el truco consiste en combinar las librerias WSE 3.0 en conjunto con WCF y crear dos clases:

public class PasswordDigestMessageInspector : IClientMessageInspector
{
    public string Username { get; set; }
    public string Password { get; set; }

    public PasswordDigestMessageInspector(string username, string password)
    {
        Username = username;
        Password = password;
    }

    #region IClientMessageInspector Members

    public void AfterReceiveReply(ref Message reply, object correlationState)
    {
        return;
    }

    public object BeforeSendRequest(ref Message request, System.ServiceModel.IClientChannel channel)
    {
        UsernameToken token = new UsernameToken(this.Username, this.Password, PasswordOption.SendHashed);

        XmlElement securityToken = token.GetXml(new XmlDocument());

        MessageHeader securityHeader = MessageHeader.CreateHeader("Security", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd", securityToken, false);
        request.Headers.Add(securityHeader);

        return Convert.DBNull;
    }

    #endregion
}

Y luego:

public class PasswordDigestBehavior : IEndpointBehavior
{

    public string Usuario { get; set; }
    public string Password { get; set; }

    public PasswordDigestBehavior(string username, string password)
    {
        Usuario = username;
        Password = password;
    }

    public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
    {
        return;
    }

    public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
    {
        clientRuntime.ClientMessageInspectors.Add(new PasswordDigestMessageInspector(Usuario, Password));
    }

    public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
    {
        return;
    }

    public void Validate(ServiceEndpoint endpoint)
    {
        // Todo bien.
        return;
    }
}

Modificar el archivo config generado por Visual Studio:

<system.serviceModel>
    <bindings>
        <basicHttpBinding>
            <binding name="SunatBinding">
                <security mode="Transport" />
            </binding>
        </basicHttpBinding>
    </bindings>
    <client>
        <endpoint address="https://www.sunat.gob.pe:443/ol-ti-itemision-otroscpe-gem-beta/billService"
            binding="basicHttpBinding" bindingConfiguration="SunatBinding"
            contract="Sunat.billService" name="ServicioSunat" />
    </client>
</system.serviceModel>

Y por último en el llamado del proxy:

public class EnvioSunat 
{
    Sunat.billServiceClient proxy;

    public Connect(string username, string password)
    {
        // Indicamos el nombre del Endpoint
        proxy = new Sunat.billServiceClient("ServicioSunat");

        // Agregamos el behavior configurado para soportar WS-Security.
        var behavior = new PasswordDigestBehavior(username, password);
        proxy.Endpoint.EndpointBehaviors.Add(behavior);

        // Abrimos el servicio.
        proxy.Open();
        /* 
            .....
            Ejecutamos el código que llame a alguna operacion del Servicio.
            .....
        */

        proxy.Close(); // Cerramos la conexión.
    }
}

Y con Fiddler, depurando el envío pude comprobar que realmente enviaba la solicitud que yo necesitaba.
He escrito un artículo al respecto en mi blog.

Respondido por: Anonymous

Leave a Reply

Your email address will not be published. Required fields are marked *