如何以编程方式设置JAX-WS客户端的SSLContext?

66
我正在处理一个分布式应用程序中的服务器,该应用程序具有浏览器客户端,并且还与第三方进行服务器之间的通信。 我的服务器具有CA签名的证书,以便让我的客户端使用TLS(SSL)通信使用HTTP/S和XMPP(安全)进行连接。一切都运行正常。
现在,我需要使用JAX-WS通过HTTPS/SSL安全连接到第三方服务器。在这种通信中,我的服务器充当JAX-WS交互中的客户端,并且我有一个由第三方签名的客户端证书。
我尝试通过标准系统配置(-Djavax.net.ssl.keyStore=xyz)添加一个新的密钥库,但是我的其他组件显然受到了影响。尽管我的其他组件正在使用专用参数进行SSL配置(my.xmpp.keystore=xxx,my.xmpp.truststore=xxy,...),但它们似乎最终使用了全局SSLContext。(配置命名空间my.xmpp.似乎表明了分离,但事实并非如此)
我还尝试将我的客户端证书添加到我的原始密钥库中,但是同样,我的其他组件似乎也不喜欢它。
我认为我唯一剩下的选择是通过编程方式钩入JAX-WS HTTPS配置,为客户端JAX-WS交互设置密钥库和信任库。
对此有什么想法/指导吗?我找到的所有信息要么使用javax.net.ssl.keyStore方法,要么设置全局SSLContext,我猜这将导致相同的冲突。我找到的最接近有用的是这个旧的错误报告,请求我需要的功能:为JAX-WS客户端运行时添加传递SSLContext的支持 有什么建议吗?

错误报告显示修复程序将在2.1.1版本中提供。 - user207421
@EJP 那个 Bug 报告更像是一个功能请求,而且没有提到它应该/将会如何完成。 - maasg
10个回答

60
这是一个难以解决的难题,以下是记录解决方法:
为了解决这个问题,需要一个自定义的KeyManager和一个使用该自定义KeyManager访问分离的KeyStore的SSLSocketFactory。我在这篇博客文章中找到了这个KeyStore和SSLFactory的基本代码: how-to-dynamically-select-a-certificate-alias-when-invoking-web-services 然后,需要将专门的SSLSocketFactory插入到WebService上下文中:
service = getWebServicePort(getWSDLLocation());
BindingProvider bindingProvider = (BindingProvider) service; 
bindingProvider.getRequestContext().put("com.sun.xml.internal.ws.transport.https.client.SSLSocketFactory", getCustomSocketFactory()); 

如果getCustomSocketFactory()方法返回使用上述方法创建的SSLSocketFactory,则JAX-WS RI(一个Java Web服务开发包)通信将通过SSL进行保护。但是,该字符串表示SSLSocketFactory属性仅适用于内置于JDK中的Sun-Oracle实现,因此只能在此实现中使用。

在这个阶段,JAX-WS服务通信通过SSL进行保护,但是如果您正在从同一个安全服务器()加载WSDL,则会遇到引导问题,因为收集WSDL的HTTPS请求将不会使用与Web服务相同的凭据。我通过让WSDL本地可用(file:///...)并动态更改Web服务端点来解决这个问题:(关于为什么需要这样做的好讨论可以在这个论坛中找到)

bindingProvider.getRequestContext().put(BindingProvider.ENDPOINT_ADDRESS_PROPERTY, webServiceLocation); 

现在WebService已经启动并能够使用命名(别名)客户端证书和双向身份验证通过SSL与服务器进行通信。∎


2
只有当你的WSDL在http://下可以访问时,这看起来才有用,但如果它也在https://下呢?客户端在第一步(获取WSDL)就失败了。 - Lukasz Frankowski
3
我们遇到了相同的情况,最终在本地拷贝了wsdl文件。否则,您就会遇到引导问题。 - maasg
6
如果您的应用程序正在使用JAXWS-RI,则应使用稍微不同的属性:com.sun.xml.ws.transport.https.client.SSLSocketFactory。我指定两个是为了满足单元测试和实际应用环境中不同的堆栈(不要问为什么)。 - mvreijn
@maasg 在提供套接字工厂的同时,我们如何强制使用TLS v1.2呢? - Crosk Cool
@maasg,你能告诉我如何“动态更改Web服务端点”吗?我有完全相同的情况,将wsdl作为文件加载,但不知道如何管理以动态方式更改ws url? - Filip Kowalski
显示剩余3条评论

23

我尝试了以下方法,但在我的环境中无法工作:

bindingProvider.getRequestContext().put("com.sun.xml.internal.ws.transport.https.client.SSLSocketFactory", getCustomSocketFactory());

但是另外一个属性完美地起了作用:

bindingProvider.getRequestContext().put(JAXWSProperties.SSL_SOCKET_FACTORY, getCustomSocketFactory());

其余代码取自第一条回复。


什么版本的所有内容?看起来不同版本的wsimport生成不同类型的代码...可能需要不同的属性名称。 - Christopher Schultz
10
当您将jaxws-rt JAR包显式添加到应用程序中时,您需要使用不含.internal.的属性名称。如果您使用JDK中包含的JAXWS-RT,则需要使用包含".internal."的属性名称。JAXWSProperties.SSL_SOCKET_FACTORY 解析为 "com.sun.xml.ws.transport.https.client.SSLSocketFactory" - Philip Helger

23

这是我根据这篇文章做出的解决方案,加入了一些微调。这个解决方案不需要创建任何额外的类。

SSLContext sc = SSLContext.getInstance("SSLv3");

KeyManagerFactory kmf =
    KeyManagerFactory.getInstance( KeyManagerFactory.getDefaultAlgorithm() );

KeyStore ks = KeyStore.getInstance( KeyStore.getDefaultType() );
ks.load(new FileInputStream( certPath ), certPasswd.toCharArray() );

kmf.init( ks, certPasswd.toCharArray() );

sc.init( kmf.getKeyManagers(), null, null );

((BindingProvider) webservicePort).getRequestContext()
    .put(
        "com.sun.xml.internal.ws.transport.https.client.SSLSocketFactory",
        sc.getSocketFactory() );

以上代码中webservicePort在哪里定义的(它在(BindingProvider)转换后的代码的最后一行中使用)? - John
@Radek,请问webservicePort的类型是什么?它看起来像是javax.xml.ws.Service,但我不确定。 - user6490459
大家好,我在2012年发布了这篇文章。老实说,我不记得我在哪里定义了那个端口。很抱歉没有解释清楚,但我已经无法访问那段代码来检查并告诉你了。我可能从一个注入的参数中获取了它,而不是自己定义它,但我不确定。如果你对BindingProvider以及它的实例化进行一些研究,应该不难弄清楚。如果当时我能让它工作,那么你也可以。祝你好运! - Radek
1
@John,webservicePort是在wsimport生成的类中定义的。 - Maforast
非常感谢 :) 这让我的一天变得更美好了! - Praveesh P

6

通过结合Radek和l0co的回答,你可以访问https背后的WSDL。

SSLContext sc = SSLContext.getInstance("TLS");

KeyManagerFactory kmf = KeyManagerFactory
        .getInstance(KeyManagerFactory.getDefaultAlgorithm());

KeyStore ks = KeyStore.getInstance("JKS");
ks.load(getClass().getResourceAsStream(keystore),
        password.toCharArray());

kmf.init(ks, password.toCharArray());

sc.init(kmf.getKeyManagers(), null, null);

HttpsURLConnection
        .setDefaultSSLSocketFactory(sc.getSocketFactory());

yourService = new YourService(url); //Handshake should succeed

getResourceAsStream(keystore) 中的 keystore 是否指证书文件的路径? - swifthorseman
在这种情况下,它是类路径内的路径。如果您需要绝对文件路径,可以使用File而不是getClass().getResourceAsStream()。 - cidus
对我来说并不是很好,因为我还会进行其他调用,我想使用默认的 SSL 套接字工厂,而这样做会使它们出现问题。 - Dave Moten
可以运行。感谢您的好回答。 - Christopher Marlowe
这对我非常有效,因为我的用例要求根据标头中的参数切换上下文。谢谢。 - adot

4
您可以将代理身份验证和SSL配置移至SOAP处理程序。
  port = new SomeService().getServicePort();
  Binding binding = ((BindingProvider) port).getBinding();
  binding.setHandlerChain(Collections.<Handler>singletonList(new ProxyHandler()));

这是我的示例,执行所有网络操作。

  class ProxyHandler implements SOAPHandler<SOAPMessageContext> {
    static class TrustAllHost implements HostnameVerifier {
      public boolean verify(String urlHostName, SSLSession session) {
        return true;
      }
    }

    static class TrustAllCert implements X509TrustManager {
      public java.security.cert.X509Certificate[] getAcceptedIssuers() {
        return null;
      }

      public void checkClientTrusted(java.security.cert.X509Certificate[] certs, String authType) {
      }

      public void checkServerTrusted(java.security.cert.X509Certificate[] certs, String authType) {
      }
    }

    private SSLSocketFactory socketFactory;

    public SSLSocketFactory getSocketFactory() throws Exception {
      // just an example
      if (socketFactory == null) {
        SSLContext sc = SSLContext.getInstance("SSL");
        TrustManager[] trustAllCerts = new TrustManager[] { new TrustAllCert() };
        sc.init(null, trustAllCerts, new java.security.SecureRandom());
        socketFactory = sc.getSocketFactory();
      }

      return socketFactory;
    }

    @Override public boolean handleMessage(SOAPMessageContext msgCtx) {
      if (!Boolean.TRUE.equals(msgCtx.get(MessageContext.MESSAGE_OUTBOUND_PROPERTY)))
        return true;

      HttpURLConnection http = null;

      try {
        SOAPMessage outMessage = msgCtx.getMessage();
        outMessage.setProperty(SOAPMessage.CHARACTER_SET_ENCODING, "UTF-8");
        // outMessage.setProperty(SOAPMessage.WRITE_XML_DECLARATION, true); // Not working. WTF?

        ByteArrayOutputStream message = new ByteArrayOutputStream(2048);
        message.write("<?xml version='1.0' encoding='UTF-8'?>".getBytes("UTF-8"));
        outMessage.writeTo(message);

        String endpoint = (String) msgCtx.get(BindingProvider.ENDPOINT_ADDRESS_PROPERTY);
        URL service = new URL(endpoint);

        Proxy proxy = Proxy.NO_PROXY;
        //Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress("{proxy.url}", {proxy.port}));

        http = (HttpURLConnection) service.openConnection(proxy);
        http.setReadTimeout(60000); // set your timeout
        http.setConnectTimeout(5000);
        http.setUseCaches(false);
        http.setDoInput(true);
        http.setDoOutput(true);
        http.setRequestMethod("POST");
        http.setInstanceFollowRedirects(false);

        if (http instanceof HttpsURLConnection) {
          HttpsURLConnection https = (HttpsURLConnection) http;
          https.setHostnameVerifier(new TrustAllHost());
          https.setSSLSocketFactory(getSocketFactory());
        }

        http.setRequestProperty("Content-Type", "application/soap+xml; charset=utf-8");
        http.setRequestProperty("Content-Length", Integer.toString(message.size()));
        http.setRequestProperty("SOAPAction", "");
        http.setRequestProperty("Host", service.getHost());
        //http.setRequestProperty("Proxy-Authorization", "Basic {proxy_auth}");

        InputStream in = null;
        OutputStream out = null;

        try {
          out = http.getOutputStream();
          message.writeTo(out);
        } finally {
          if (out != null) {
            out.flush();
            out.close();
          }
        }

        int responseCode = http.getResponseCode();
        MimeHeaders responseHeaders = new MimeHeaders();
        message.reset();

        try {
          in = http.getInputStream();
          IOUtils.copy(in, message);
        } catch (final IOException e) {
          try {
            in = http.getErrorStream();
            IOUtils.copy(in, message);
          } catch (IOException e1) {
            throw new RuntimeException("Unable to read error body", e);
          }
        } finally {
          if (in != null)
            in.close();
        }

        for (Map.Entry<String, List<String>> header : http.getHeaderFields().entrySet()) {
          String name = header.getKey();

          if (name != null)
            for (String value : header.getValue())
              responseHeaders.addHeader(name, value);
        }

        SOAPMessage inMessage = MessageFactory.newInstance()
          .createMessage(responseHeaders, new ByteArrayInputStream(message.toByteArray()));

        if (inMessage == null)
          throw new RuntimeException("Unable to read server response code " + responseCode);

        msgCtx.setMessage(inMessage);
        return false;
      } catch (Exception e) {
        throw new RuntimeException("Proxy error", e);
      } finally {
        if (http != null)
          http.disconnect();
      }
    }

    @Override public boolean handleFault(SOAPMessageContext context) {
      return false;
    }

    @Override public void close(MessageContext context) {
    }

    @Override public Set<QName> getHeaders() {
      return Collections.emptySet();
    }
  }

这里使用了 UrlConnection,您可以在处理程序中使用任何库。玩得开心!


3

上述内容是没问题的(如我在评论中所说),除非你的WSDL也能通过https访问。

以下是我的解决方法:

将SSLSocketFactory设置为默认值:

HttpsURLConnection.setDefaultSSLSocketFactory(...);

对于我使用的Apache CXF,您还需要将以下行添加到配置中:
<http-conf:conduit name="*.http-conduit">
  <http-conf:tlsClientParameters useHttpsURLConnectionDefaultSslSocketFactory="true" />
<http-conf:conduit>

1
我在设置信任管理器时遇到了自签名证书的问题。我使用了apache httpclient的SSLContexts构建器来创建一个自定义的SSLSocketFactory。
SSLContext sslcontext = SSLContexts.custom()
        .loadKeyMaterial(keyStoreFile, "keystorePassword.toCharArray(), keyPassword.toCharArray())
        .loadTrustMaterial(trustStoreFile, "password".toCharArray(), new TrustSelfSignedStrategy())
        .build();
SSLSocketFactory customSslFactory = sslcontext.getSocketFactory()
bindingProvider.getRequestContext().put(JAXWSProperties.SSL_SOCKET_FACTORY, customSslFactory);

loadTrustMaterial方法中传入new TrustSelfSignedStrategy()作为参数。

1
尝试了仍无法工作的人,我在Wildfly 8中使用动态 Dispatcher 成功实现了以下内容:

bindingProvider.getRequestContext().put("com.sun.xml.ws.transport.https.client.SSLSocketFactory", yourSslSocketFactory);

请注意,在此处Property键的 internal 部分已被删除。


我使用Wildfly 8,但对我来说也不起作用。(你说的动态调度器是什么意思?- 请查看我的帖子:https://dev59.com/HJbfa4cB1Zd3GeqP0_Iq和https://dev59.com/HJbfa4cB1Zd3GeqP0_Iq) - badera

1
我们遇到了一个问题,由于系统集成之间keystore的冲突,所以我们使用了以下代码。
private PerSecurityWS prepareConnectionPort()  {
      final String HOST_BUNDLE_SYMBOLIC_NAME = "wpp.ibm.dailyexchangerates";
      final String PATH_TO_SLL = "ssl/<your p.12 certificate>";
      final File ksFile = getFile(HOST_BUNDLE_SYMBOLIC_NAME, PATH_TO_SLL);
      final String serverURI = "you url";


      final KeyStore keyStore = KeyStore.getInstance("pkcs12");
      keyStore.load(new FileInputStream(ksFile.getAbsolutePath()), keyStorePassword.toCharArray());
      final KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
      kmf.init(keyStore, keyStorePassword.toCharArray());

      final HostnameVerifier DO_NOT_VERIFY = new HostnameVerifier() {
        @Override
        public boolean verify(final String hostname, final SSLSession session) {
          return false;
        }
      };

      final SSLContext ctx = SSLContext.getInstance("TLS");
      ctx.init(kmf.getKeyManagers(), null, null);
      final SSLSocketFactory sslSocketFactory = ctx.getSocketFactory();

      final PerSecurityWS port = new PerSecurityWS_Service().getPerSecurityWSPort();

      final BindingProvider bindingProvider = (BindingProvider) port;
      bindingProvider.getRequestContext().put("com.sun.xml.internal.ws.transport.https.client.SSLSocketFactory",sslSocketFactory);
      bindingProvider.getRequestContext().put(BindingProvider.ENDPOINT_ADDRESS_PROPERTY, serverURI);
      bindingProvider.getRequestContext().put("com.sun.xml.internal.ws.transport.https.client.hostname.verifier",DO_NOT_VERIFY);
      return port;
    }

0

3
好的,我会尽力完成翻译任务。需要翻译的内容是:“请在链接中引用最相关的部分,以防目标网站无法访问或永久下线。” - Błażej Michalik
这个答案将为整个应用程序设置信任存储。相关部分:-Djavax.net.ssl.trustStore=$JAVA_HOME/jre/lib/security/cacerts -Djavax.net.ssl.trustStorePassword=password - EpicVoyage

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