最近踩了 WebService 一个的坑。与其他部门有个常规的 WebService 接口,对方提供服务,这边调用。像往常一样,使用 cxf 生成了客户端的代码并调用,开发的过程中并没有遇到什么问题,测试也没发现问题。然而一个礼拜之后正式上线的生产环境中,客户端调用接口总是抛出异常 服务器发送了 HTTP 状态代码 302: Moved Temporarily

    @Test
    public void test() throws Exception {

        URL url = new URL("https://home.winning.com/web-service/cxf/findReportEntity?wsdl");

        ReportEntityService service = new ReportEntityServiceImplService(url)
                .getReportEntityServiceImplPort();

        ReportEntity res = service.findOne("");
        System.out.println(res.getEnterpirseName());
    }
com.sun.xml.internal.ws.client.ClientTransportException: 服务器发送了 HTTP 状态代码 302: Moved Temporarily
	at com.sun.xml.internal.ws.transport.http.client.HttpTransportPipe.checkStatusCode(HttpTransportPipe.java:310)
	at com.sun.xml.internal.ws.transport.http.client.HttpTransportPipe.createResponsePacket(HttpTransportPipe.java:259)
	at com.sun.xml.internal.ws.transport.http.client.HttpTransportPipe.process(HttpTransportPipe.java:217)
	at com.sun.xml.internal.ws.transport.http.client.HttpTransportPipe.processRequest(HttpTransportPipe.java:130)
	at com.sun.xml.internal.ws.transport.DeferredTransportPipe.processRequest(DeferredTransportPipe.java:95)
	at com.sun.xml.internal.ws.api.pipe.Fiber.__doRun(Fiber.java:1121)
	at com.sun.xml.internal.ws.api.pipe.Fiber._doRun(Fiber.java:1035)
	at com.sun.xml.internal.ws.api.pipe.Fiber.doRun(Fiber.java:1004)
	at com.sun.xml.internal.ws.api.pipe.Fiber.runSync(Fiber.java:862)
	at com.sun.xml.internal.ws.client.Stub.process(Stub.java:448)
	at com.sun.xml.internal.ws.client.sei.SEIStub.doProcess(SEIStub.java:178)
	at com.sun.xml.internal.ws.client.sei.SyncMethodHandler.invoke(SyncMethodHandler.java:93)
	at com.sun.xml.internal.ws.client.sei.SyncMethodHandler.invoke(SyncMethodHandler.java:77)
	at com.sun.xml.internal.ws.client.sei.SEIStub.invoke(SEIStub.java:147)
	at com.sun.proxy.$Proxy32.findOne(Unknown Source)
	at SkytechTest.test(SkytechTest.java:30)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)

问题排查


检查了下配置的 URL 发现,实际配置的地址是HTTPS形式的。因为测试环境与生产环境的配置都是一样的,除了URL,测试环境是直接通过 IP 访问的。基本上可以确定问题就出在HTTPS上了。初步判断,应该是对方在生产环境中使用了 nginx 做了反向代理,并且开启了 SSL,导致访问接口的时候被重定向了。但是,wsdl 是可以通过 https 访问的,这就有些奇怪了。 为了排查问题,用C# 快速写了一个测试程序:

ReportEntityServiceClient client = new ReportEntityServiceClient();
try
{
    reportEntity report = client.findOne("");
    Console.WriteLine(report.enterpirseName);
}
catch (Exception ex)
{
     Console.WriteLine(ex.Message);
}

同样抛出了异常,异常消息与 Java 有些不同, No binding operation info while invoking unknown method with params unknown., 将 app.config 文件中的 endpoint address 节点修改为 https, 运行提示:提供的 URI 方案“https”无效,应为“http”。参数名: via 问题可以确定,就是 https 导致的问题。通过 Wireshark 抓包来看,接口调用请求的为 http,虽然配置的 URL 是 https 但是实际上调用的其实是 http。

解决方法


如果 WebService 调用可以设置代理,把代理地址设置成 https 应该就能解决问题。 经过一番 Google 貌似找到一个解决方法, 通过 BindingProvider 拿到请求上下文,添加 BindingProvider.ENDPOINT_ADDRESS_PROPERTY:

    @Test
    public void test() throws Exception {
        String wsdlLocation = "https://home.winning.com/web-service/cxf/findReportEntity?wsdl";
        URL url = new URL(wsdlLocation);

        ReportEntityService service = new ReportEntityServiceImplService(url)
                .getReportEntityServiceImplPort();
        ((BindingProvider) service).getRequestContext()
                .put(BindingProvider.ENDPOINT_ADDRESS_PROPERTY, wsdlLocation);
        ReportEntity res = service.findOne("");
        System.out.println(res.getEnterpirseName());
    }

问题就这么解决了,只需要添加一行代码就搞定了。对于 .NET 需要修改 app.config , 在 binding 节点中添加 security 配置,并且将 endpoint address 节点修改为 https。详细配置如下:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <system.serviceModel>
    <bindings>
      <basicHttpBinding>
        <binding name="ReportEntityServiceImplServiceSoapBinding" >
          <security mode="Transport">
            <transport clientCredentialType="None" proxyCredentialType="None" realm=""/>
            <message clientCredentialType="Certificate" algorithmSuite="Default" />
          </security>
        </binding>
      </basicHttpBinding>
    </bindings>
    <client>
      <endpoint address="https://home.winning.com/web-service/cxf/findReportEntity"
          binding="basicHttpBinding" bindingConfiguration="ReportEntityServiceImplServiceSoapBinding"
          contract="ServiceReference.ReportEntityService" name="ReportEntityServiceImplPort" />
    </client>
  </system.serviceModel>
</configuration>

总结分析


BindingProvider 定义了获取 request 与 response 上下文 (context)的方法,context 是一个 Map, 可以设置一些属性。 除了 ENDPOINT_ADDRESS_PROPERTY 以外,还有 USERNAME_PROPERTYPASSWORD_PROPERTY 等属性。 参考 Purpose of BindingProvider in JAX-WS web service