在服务器端记录SOAP请求和响应

9
我正在尝试为我的ASP.NET webservice创建一个记录所有SOAP方法调用的日志服务。我已经查看了从控制台应用程序记录SOAP消息和MSDN上SOAP扩展的演示步骤(http://msdn.microsoft.com/en-us/library/s25h0swd%28v=vs.100%29.aspx),但它们似乎没有完全覆盖此问题。
我不想改变SOAP消息,只是将其记录到数据库表中。我正在尝试读取SOAP消息流,解析它作为XML,记录XML,然后让调用继续执行。但是当我读取流时,它就被释放/丢弃了。我尝试过复制流内容以避免打断流程。
根据演示步骤,ProcessMessage方法应该类似于以下内容:
public override void ProcessMessage(SoapMessage message) 
{
   switch (message.Stage) 
   {
   case SoapMessageStage.BeforeSerialize:
       break;
   case SoapMessageStage.AfterSerialize:
       // Write the SOAP message out to a file.
       WriteOutput( message );
       break;
   case SoapMessageStage.BeforeDeserialize:
       // Write the SOAP message out to a file.
       WriteInput( message );
       break;
   case SoapMessageStage.AfterDeserialize:
       break;
   default:
       throw new Exception("invalid stage");
   }
}

BeforeDeserialize 阶段中,我成功地解析了流,但是在 AfterSerialize 阶段中再次调用 ProcessMessage,此时流已被使用,并且不再包含任何数据。

根据 SOAP 扩展使用 SOAP 消息修改 (http://msdn.microsoft.com/en-us/library/esw638yk%28v=vs.100%29.aspx) 的说明,SOAP 调用经过以下步骤:

服务器端接收请求消息并准备响应

  1. Web 服务器上的 ASP.NET 接收 SOAP 消息。
  2. 在 Web 服务器上创建了 SOAP 扩展的新实例。
  3. 如果这是此 SOAP 扩展在服务器上第一次与此 Web 服务一起执行,则在服务器上运行的 SOAP 扩展上调用 GetInitializer 方法。
  4. 调用 Initialize 方法。
  5. 调用 ChainStream 方法。
  6. 使用 SoapMessageStage 设置为 BeforeDeserialize 调用 ProcessMessage 方法。
  7. ASP.NET 反序列化 XML 中的参数。
  8. 使用 SoapMessageStage 设置为 AfterDeserialize 调用 ProcessMessage 方法。
  9. ASP.NET 创建实现 Web 服务的类的新实例,并调用 Web 服务方法,将反序列化的参数传递给该方法。该对象驻留在与 Web 服务器相同的计算机上。
  10. Web 服务方法执行其代码,最终设置返回值和任何 out 参数。
  11. 使用 SoapMessageStage 设置为 BeforeSerialize. 调用 ProcessMessage 方法。
  12. Web 服务器上的 ASP.NET 将返回值和 out 参数序列化为 XML。
  13. 使用 SoapMessageStage 设置为 AfterSerialize 调用 ProcessMessage 方法。
  14. ASP.NET 将 SOAP 响应消息通过网络发送回 Web 服务客户端。
第6步执行正确,SOAP XML已记录。然后在服务器处理调用、完成所需操作(第10步)并返回响应(第13步)之前,它不应该再做任何事情。但是,它立即在AfterSerialize阶段再次调用ProcessMessage,但这次流已经被使用完毕,当我尝试记录它时会抛出异常。
根据演练,我应该使用ChainStream方法,并且应该在上面的第5步中运行。当我调用它时,它会运行两次,一次在BeforeDeserialize之前,一次在AfterSerialize之前。
我已经尝试将消息流复制到单独的流中并将其用于日志记录,还尝试设置某种状态以判断是否已经运行了BeforeDeserialize,但问题仍然存在。
我仍然需要在AfterSerialize中编写代码来处理发送回客户端的响应。但是,如果我尝试删除AfterSerialize中的代码,只运行BeforeDeserialize中的代码,我会得到一个`HTTP 400: Bad Request`。

所有这些操作都发生在实际方法调用之前,所以我甚至没有机会进入方法内部的代码(步骤10)。


好的代码,使用VB编写,但很容易转换为C#:https://github.com/Apress/pro-asp.net-1.1-in-vb-.net/blob/master/source/Chapter25/SoapLog.vb - Leszek P
2个回答

11

我的解决方案基于mikebridge的方案,但我不得不进行一些修改。必须包括初始化程序,并且如果您在不可用阶段尝试访问SOAP消息信息,则会抛出异常。

public class SoapLoggingExtension : SoapExtension
{
    private Stream _originalStream;
    private Stream _workingStream;
    private static String _initialMethodName;
    private static string _methodName;
    private static String _xmlResponse;

    /// <summary>
    /// Side effects: saves the incoming stream to
    /// _originalStream, creates a new MemoryStream
    /// in _workingStream and returns it.  
    /// Later, _workingStream will have to be created
    /// </summary>
    /// <param name="stream"></param>
    /// <returns></returns>
    public override Stream ChainStream(Stream stream)
    {

        _originalStream = stream;
        _workingStream = new MemoryStream();
        return _workingStream;
    }

    /// <summary>
    /// Process soap message
    /// </summary>
    /// <param name="message"></param>
    public override void ProcessMessage(SoapMessage message)
    {
        switch (message.Stage)
        {
            case SoapMessageStage.BeforeSerialize:
                break;

            case SoapMessageStage.AfterSerialize:
                //Get soap call as a xml string
                var xmlRequest = GetSoapEnvelope(_workingStream);
                //Save the inbound method name
                _methodName = message.MethodInfo.Name;
                CopyStream(_workingStream, _originalStream);
                //Log call
                LogSoapRequest(xmlRequest, _methodName, LogObject.Direction.OutPut);
                break;

            case SoapMessageStage.BeforeDeserialize:
                CopyStream(_originalStream, _workingStream);
                //Get xml string from stream before it is used
                _xmlResponse = GetSoapEnvelope(_workingStream);
                break;

            case SoapMessageStage.AfterDeserialize:
                //Method name is only available after deserialize
                _methodName = message.MethodInfo.Name;
                LogSoapRequest(_xmlResponse, _methodName, LogObject.Direction.InPut);
                break;
        }
    }

    /// <summary>
    /// Returns the XML representation of the Soap Envelope in the supplied stream.
    /// Resets the position of stream to zero.
    /// </summary>
    private String GetSoapEnvelope(Stream stream)
    {
        stream.Position = 0;
        StreamReader reader = new StreamReader(stream);
        String data = reader.ReadToEnd();
        stream.Position = 0;
        return data;
    }

    private void CopyStream(Stream from, Stream to)
    {
        TextReader reader = new StreamReader(from);
        TextWriter writer = new StreamWriter(to);
        writer.WriteLine(reader.ReadToEnd());
        writer.Flush();
    }

    public override object GetInitializer(Type serviceType)
    {
        return serviceType.FullName;
    }

    //Never needed to use this initializer, but it has to be implemented
    public override object GetInitializer(LogicalMethodInfo methodInfo, SoapExtensionAttribute attribute)
    {
        throw new NotImplementedException();
        //return ((TraceExtensionAttribute)attribute).Filename;
    }

    public override void Initialize(object initializer)
    {
        if (String.IsNullOrEmpty(_methodName))
        {
            _initialMethodName = _methodName;
            _waitForResponse = false;
        }
    }

    private void LogSoapRequest(String xml, String methodName, LogObject.Direction direction)
    {

        String connectionString = String.Empty;
        String callerIpAddress = String.Empty;
        String ipAddress = String.Empty;

        try
        {
            //Only log outbound for the response to the original call
            if (_waitForResponse && xml.IndexOf("<" + _initialMethodName + "Response") < 0)
            {
                return;
            }

            if (direction == LogObject.Direction.InPut) {
                _waitForResponse = true;
                _initialMethodName = methodName;
            }

            connectionString = GetSqlConnectionString();
            callerIpAddress = GetClientIp();
            ipAddress = GetClientIp(HttpContext.Current.Request.UserHostAddress);

            //Log call here

            if (!String.IsNullOrEmpty(_methodName) && xml.IndexOf("<" + _initialMethodName + "Response") > 0)
            {
                //Reset static values to initial
                _methodName = String.Empty;
                _initialMethodName = String.Empty;
                _waitForResponse = false;
            }
        }
        catch (Exception ex)
        {
            //Error handling here
        }
    }
    private static string GetClientIp(string ip = null)
    {
        if (String.IsNullOrEmpty(ip))
        {
            ip = HttpContext.Current.Request.ServerVariables["HTTP_X_FORWARDED_FOR"];
        }
        if (String.IsNullOrEmpty(ip) || ip.Equals("unknown", StringComparison.OrdinalIgnoreCase))
        {
            ip = HttpContext.Current.Request.ServerVariables["REMOTE_ADDR"];
        }
        if (ip == "::1")
            ip = "127.0.0.1";
        return ip;
    }
}

methodName变量用于确定我们正在等待哪个入站调用的响应。当然,这是可选的,但在我的解决方案中,我对其他web服务进行了几次调用,但我只想记录第一次调用的响应。

第二部分是您需要将正确的行添加到web.config中。显然,不包括整个类类型定义会导致敏感问题(在此示例中仅定义了类名,这是不起作用的。类从未初始化):

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.web>
    <webServices>
        <soapExtensionTypes>
            <add group="High" priority="1" type="WsNs.SoapLoggingExtension, WsNs, Version=1.0.0.0, Culture=neutral" />
        </soapExtensionTypes>
    </webServices>
</system.web>
</configuration>

抱歉,是的,我忘记了XML注册。如果我有更多这样的工作要做,我会在基于事件的框架中重写整个混乱代码,以便事件处理程序永远不会获取未初始化的数据。 - mikebridge
2
您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - FEST
对我也不起作用... .NET 4.7.2 - Vaibhav Garg

2
这是我的第一次尝试,灵感来自于thisthis
SoapExtension在处理流和变量初始化或未初始化时有各种副作用和隐藏的时间依赖性,因此这是脆弱的代码。我发现关键是在恰当的时刻将原始流复制到内存流中,然后再将其返回原始流中。
public class SoapLoggingExtension : SoapExtension
{

    private Stream _originalStream;
    private Stream _workingStream;
    private string _methodName;
    private List<KeyValuePair<string, string>> _parameters;
    private XmlDocument _xmlResponse;
    private string _url;

    /// <summary>
    /// Side effects: saves the incoming stream to
    /// _originalStream, creates a new MemoryStream
    /// in _workingStream and returns it.  
    /// Later, _workingStream will have to be created
    /// </summary>
    /// <param name="stream"></param>
    /// <returns></returns>
    public override Stream ChainStream(Stream stream)
    {

        _originalStream = stream;
        _workingStream = new MemoryStream();
        return _workingStream;
    }

    /// <summary>
    /// AUGH, A TEMPLATE METHOD WITH A SWITCH ?!?
    /// Side-effects: everywhere
    /// </summary>
    /// <param name="message"></param>
    public override void ProcessMessage(SoapMessage message)
    {
        switch (message.Stage)
        {
            case SoapMessageStage.BeforeSerialize:
                break;

            case SoapMessageStage.AfterSerialize:

                var xmlRequest = GetSoapEnvelope(_workingStream);
                CopyStream(_workingStream, _originalStream);
                LogResponse(xmlRequest, GetIpAddress(), _methodName, _parameters); // final step
                break;

            case SoapMessageStage.BeforeDeserialize:

                CopyStream(_originalStream, _workingStream);
                _xmlResponse = GetSoapEnvelope(_workingStream);
                _url = message.Url;
                break;

            case SoapMessageStage.AfterDeserialize:

                SaveCallInfo(message);                    
                break;
        }
    }

    private void SaveCallInfo(SoapMessage message)
    {
        _methodName = message.MethodInfo.Name;

        // the parameter value is converted to a string for logging, 
        // but this may not be suitable for all applications.
        ParameterInfo[] parminfo = message.MethodInfo.InParameters;
        _parameters = parminfo.Select((t, i) => new KeyValuePair<string, String>(
                t.Name, Convert.ToString(message.GetInParameterValue(i)))).ToList();

    }

    private void LogResponse(
        XmlDocument xmlResponse,
        String ipaddress,
        string methodName, 
        IEnumerable<KeyValuePair<string, string>> parameters)
    {
        // SEND TO LOGGER HERE!
    }

    /// <summary>
    /// Returns the XML representation of the Soap Envelope in the supplied stream.
    /// Resets the position of stream to zero.
    /// </summary>
    private XmlDocument GetSoapEnvelope(Stream stream)
    {
        XmlDocument xml = new XmlDocument();
        stream.Position = 0;
        StreamReader reader = new StreamReader(stream);
        xml.LoadXml(reader.ReadToEnd());
        stream.Position = 0;
        return xml;
    }

    private void CopyStream(Stream from, Stream to)
    {
        TextReader reader = new StreamReader(from);
        TextWriter writer = new StreamWriter(to);
        writer.WriteLine(reader.ReadToEnd());
        writer.Flush();
    }

    // GLOBAL VARIABLE DEPENDENCIES HERE!!
    private String GetIpAddress()
    {
        try
        {
            return HttpContext.Current.Request.UserHostAddress;
        }
        catch (Exception)
        {
            // ignore error;
            return "";
        }
    }

我发现这个代码还拦截了出站的SOAP调用---在这种情况下,如果你尝试在AfterDeserialize内部调用GetInParameterValue,这段代码将抛出异常"The value may not be accessed in message stage AfterDeserialize"。如果你有SoapClientMessages,你可以在逻辑中包装一个"if (message is SoapServerMessage)"来防止它拦截它们。 - mikebridge
1
感谢您的帮助!我成功地使用您的示例作为起点,解决了问题。上面的示例缺少SoapExtension类的初始化程序,这有点令人困惑。我使用了在此示例中找到的初始化程序。另外,为确保扩展程序被使用,您需要将一些行添加到web.config文件中(使用此示例作为起点)。我会将自己的解决方案添加为答案。 - kumaheiyama

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接