当遇到302响应时,为什么WCF调用SOAP服务失败?

8
我已经编写了一个应用程序,从开始就使用WCF调用进行登录。我使用服务引用生成了客户端代码。对于将其服务安装在本地网络上的客户端来说,它运行良好。然而,在SaaS环境中,这些相同的服务也受到公司权力控制。在SaaS环境中,我被告知登录失败了。通过使用Fiddler进行调查,我发现用于登录的服务调用返回HTML,具体来说,是列出所有可用方法的网页,从.asmx文件中。
SaaS环境有一个小问题可能会导致问题,但我不知道如何验证这是否是问题,如果是问题,也不知道如何解决。问题是服务器重定向(302)调用。
客户端代码:
    client.Endpoint.Address = new EndpointAddress("http://" + settings.MyURL + "/myProduct/webservices/webapi.asmx");
    client.DoLogin(username, password);
发送到服务器之前的原始数据,包括s:Envelope XML标记。请注意,在发送到重定向服务器时缺少s:Envelope XML标记:
    GET https://www.myurl.com/myProduct/webservices/webapi.asmx HTTP/1.1
    Content-Type: text/xml; charset=utf-8
    VsDebuggerCausalityData: uIDPo7TgjY1gCLFLu6UXF8SWAoEAAAAAQxHTAupeAkWz2p2l3jFASiUPHh+L/1xNpTd0YqI2o+wACQAA
    SOAPAction: "http://Interfaces.myProduct.myCompany.com/DoLogin"
Accept-Encoding: gzip, deflate
    Host: www.gotimeforce2.com
    Connection: Keep-Alive
我该如何让这个愚蠢的东西工作?
编辑:值得注意的是,我正在使用WCF/svcutil.exe/service-reference而不是较旧的ASMX/wsdl.exe/web-reference。否则,对于未来的读者来说,Raj提出的wsdl解决方案会是一个很好的解决方案。如果您遇到此问题并使用wsdl技术,请查看Raj的优秀答案。
编辑2:在进行了大量关于WCF和302的研究之后,听起来它们根本不能很好地协同工作,也没有简单的方法提供给WCF API自定义代码来处理这种情况。由于我无法控制服务器,因此我已经吸收并将我的API重新生成为Web引用,并使用了Raj的解决方案。
编辑3:更新标题以更好地反映解决方案,现在已经了解了问题的原因。原始标题:为什么WCF在重定向时不包括s:Envelope?

进一步调查表明,许多可能会遇到服务器返回302的代码实现错误地在第二次提交中强制执行GET而不是原始的HTTP动词——在我的情况下是POST。GET不发送参数。这就是“为什么”,但我仍然需要知道“如何”修复它。 - REW
1个回答

6
好的,我仔细调查了一下,并试图在我的这一侧复制此问题。我能够复制该问题并找到解决方法。然而,我不确定这是否适用于您的情况,因为它取决于与负载均衡器管理团队进行接口操作。以下是我的发现。
在查看http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html时,您会注意到HTTP状态代码302和303的说明中有以下添加说明。 302 Found
  Note: RFC 1945 and RFC 2068 specify that the client is not allowed
  to change the method on the redirected request.  However, most
  existing user agent implementations treat 302 as if it were a 303
  response, performing a GET on the Location field-value regardless
  of the original request method. The status codes 303 and 307 have
  been added for servers that wish to make unambiguously clear which
  kind of reaction is expected of the client.

303 See Other

  Note: Many pre-HTTP/1.1 user agents do not understand the 303
  status. When interoperability with such clients is a concern, the
  302 status code may be used instead, since most user agents react
  to a 302 response as described here for 303.

进一步查看 http://en.wikipedia.org/wiki/List_of_HTTP_status_codes,您会注意到以下对于 HTTP 状态码 302、303 和 307 的解释。

302 Found:这是业界实践与标准相矛盾的一个例子。HTTP/1.0 规范(RFC 1945)要求客户端执行临时重定向(最初的描述短语为“暂时移动”),但流行的浏览器使用 302 来实现 303 See Other 的功能。因此,HTTP/1.1 添加了状态码 303 和 307 来区分这两种行为。然而,一些 Web 应用和框架使用 302 状态码似乎就像是 303 状态码。

303 See Other (自 HTTP/1.1 开始):响应请求可以使用 GET 方法在另一个 URI 下找到。如果响应是针对 POST(或 PUT/DELETE)请求的,则应该假定服务器已经收到数据并且应该使用单独的 GET 消息发出重定向。这是正常的客户端/服务器交互中的基本流程。

307 Temporary Redirect (自 HTTP/1.1 开始):在这种情况下,请求应该使用另一个 URI 重复发送;但是,将来的请求仍应该使用原始 URI。与历史上如何实现 302 不同,重新发出原始请求时不允许更改请求方法。例如,应该使用另一个 POST 请求重复 POST 请求。

因此,根据这个解释,我们能够解释 WCF 调用的行为,该调用在 302 重定向时发送了一个没有 s:Envelope 的 GET 请求。这将在客户端端失败。

最简单的修复方法是让服务器在响应中返回 307 临时重定向而不是 302 Found 状态码。这就需要您寻求负责负载均衡器上重定向规则的服务器团队的帮助。我在本地进行了测试,使用 Service Reference 消费服务的客户端代码无缝执行调用,即使使用 307 临时重定向。

实际上,您可以使用我在 Github 上上传的解决方案 Here 测试所有这些。我已更新这个项目来说明使用服务引用而不是生成的 wsdl 代理类来消费 asmx 服务。

但是,如果在您的环境中从 302 Found 到 307 Temporary Redirect 的更改不可行,则建议使用 Solution 1(无论响应状态码是 302 还是 307 都不应该有问题)或使用我的 original answer,通过直接访问配置文件中的正确 URL 来解决此问题。希望这可以帮到您!

Solution 1

如果您在生产环境中无法访问配置文件,或者您不想在配置文件中使用多个URL,那么可以使用以下方法。链接到包含示例解决方案的Github存储库点击此处
基本上,如果您注意到wsdl.exe自动生成的文件,就会注意到服务代理类派生自System.Web.Services.Protocols.SoapHttpClientProtocol。该类有一个受保护的方法System.Net.WebRequest GetWebRequest(Uri uri),您可以覆盖它。在这里,您可以添加一个方法来检查302临时重定向是否是HttpWebRequest.GetResponse()方法的结果。如果是,则可以将Url设置为响应Location头返回的新Url,如下所示。 this.Url = new Uri(uri, response.Headers["Location"]).ToString(); 因此,创建一个名为SoapHttpClientProtocolWithRedirect的类。
public class SoapHttpClientProtocolWithRedirect :
    System.Web.Services.Protocols.SoapHttpClientProtocol
{
    protected override System.Net.WebRequest GetWebRequest(Uri uri)
    {
        if (!_redirectFixed)
        {
            FixRedirect(new Uri(this.Url));
            _redirectFixed = true;

            return base.GetWebRequest(new Uri(this.Url));
        }

        return base.GetWebRequest(uri);
    }

    private bool _redirectFixed = false;
    private void FixRedirect(Uri uri)
    {
        var request = (HttpWebRequest)WebRequest.Create(uri);
        request.CookieContainer = new CookieContainer();
        request.AllowAutoRedirect = false;
        var response = (HttpWebResponse)request.GetResponse();

        switch (response.StatusCode)
        {
            case HttpStatusCode.Redirect:
            case HttpStatusCode.TemporaryRedirect:
            case HttpStatusCode.MovedPermanently:
                this.Url = new Uri(uri, response.Headers["Location"]).ToString();
                break;
        }
    }
}

现在,让我们来谈谈手动使用wsdl.exe生成代理类相对于使用服务引用的优势。在手动创建的代理类中,修改类声明:从原先的`

`改为:
public partial class WebApiProxy : System.Web.Services.Protocols.SoapHttpClientProtocol

to

public partial class WebApiProxy : SoapHttpClientProtocolWithRedirect

现在按照以下方式调用DoLogin方法。
var apiClient = new WebApiProxy(GetServiceUrl());
//TODO: Add any required headers etc.
apiClient.DoLogin(username,password);

您会注意到,302重定向是由您的SoapHttpClientProtocolWithRedirect类中的代码平稳处理的。
另一个优点是这样做,您不必担心其他开发人员会刷新服务引用并丢失您手动生成的代理类所做的更改。希望这可以帮助您。
原始答案:
为什么不在配置文件中包含生产/本地服务的整个url呢?这样,您可以在适当的位置使用适当的url启动调用。
此外,我建议在任何用于生产的代码中避免使用服务引用。一种使用asmx服务而无需服务引用的方法是使用wsdl.exe工具生成WebApiProxy.cs文件。现在,您可以将WebApiProxy.cs文件包含在项目中,并按如下所示进行实例化。
var apiClient = new WebApiProxy(GetServiceUrl());
//TODO: Add any required headers etc.
apiClient.DoLogin(username,password);

这里是GetServiceUrl()方法。使用配置存储库进一步解耦和提高可测试性。

private string GetServiceUrl()
    {
        try
        {
            return
            _configurationRepository.AppSettings[
                _configurationRepository.AppSettings["WebApiInstanceToUse"]];
        }
        catch (NullReferenceException ex)
        {
            //TODO: Log error
            return string.Empty;
        }
    }

那么您的配置文件可以在 部分中包含以下信息。
<add key="StagingWebApiInstance" value="http://mystagingserver/myProduct/webservices/webapi.asmx "/>
<add key="ProductionWebApiInstance" value="https://www.myurl.com/myProduct/webservices/webapi.asmx"/>
<!-- Identify which WebApi.asmx instance to Use-->
<add key="WebApiInstanceToUse" value="ProductionWebApiInstance"/>

另外,我建议避免使用“+”运算符连接字符串。当只使用一次时,它不会对性能产生太大的影响,但如果在代码中多次使用这种连接方式,与使用 StringBuilder 相比,执行时间会有很大差异。请查看 http://msdn.microsoft.com/en-us/library/ms228504.aspx 以了解更多关于为什么使用 StringBuilder 可以提高性能的信息。

测试性评论是一个很好的观点。 字符串连接对我的目的来说可能不是什么大问题。 Visual Studio 2010似乎调用wsdl.exe生成服务引用。 - REW
我用另一种解决方案更新了答案,顺便也解释了为什么手动使用wsdl.exe生成代理类是更灵活的方法。正如我在原始帖子中提到的那样,使用+重载进行一次连接并不会产生太大影响。我建议使用StringBuilder作为好的实践,因为在企业环境中的开发人员倾向于复制他们看到的现有代码,并证明这是“接受”的做法,因为曾经有人这样做过。 - Rajaraam Murali
StringBuilder注释:啊,我懂了!wsdl.exe注释:经过进一步的调查生成的代码,我正在使用WCF和svcutil.exe,而不是旧技术ASMX和wsdl.exe。除了这个我之前没有意识到的信息(我正在使用WCF),你更新的解决方案本来可以很好地工作。 - REW
好的,从我所看到的一切来看,WCF和302根本不兼容。我已经放弃了,并重新将我的API作为Web引用而不是服务引用进行了重做。叹气 - REW
@REW 抱歉让你等了一会儿,但我已经更新了我的答案。请看一下,它可能提供了多种使用 Web 引用的替代方案。 - Rajaraam Murali
@REW 如果您不介意的话,我想建议您稍微修改问题,以反映“为什么WCF在302 HTTP重定向时执行GET而不是POST”或类似的内容。 s:Envelope仅适用于基于SOAP的服务。 - Rajaraam Murali

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