如何使用 spring-ws 客户端使用不同的 keystore 调用同一个 webservice

How to use spring-ws client to call the same webservice using different keystore

我有一些应用程序需要 运行 在同一个应用程序服务器中。每个应用程序都需要使用特定于该应用程序的证书通过相同的 Web 服务进行身份验证。 显然我可以将所有证书放在同一个密钥库中,但我如何指定我必须使用哪一个? 对于我使用 Spring WebServiceTemplate 的调用,我想找到可以在 spring xml 配置文件中轻松配置的内容。

我正在努力遵循这个: How can I have multiple SSL certificates for a Java server

整个概念很清楚,但我无法理解如何使用 Spring WebServiceTemplate link 以及如何在调用中指定我必须使用哪个证书。

我找到了解决办法。它不完美,也不完全干净。 我需要更多测试以确保它正常工作,目前它是 运行.

这就是神奇的 FactoryBean "CustomSSLHttpClientFactory.java"。

package foo.bar.services;
import java.io.InputStream;
import java.net.Socket;
import java.security.KeyStore;
import java.util.Map;

import javax.net.ssl.SSLContext;

import org.apache.http.client.HttpClient;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.HttpClientConnectionManager;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.PrivateKeyDetails;
import org.apache.http.conn.ssl.PrivateKeyStrategy;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLContextBuilder;
import org.apache.http.conn.ssl.SSLContexts;
import org.apache.http.conn.ssl.TrustSelfSignedStrategy;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.BasicHttpClientConnectionManager;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.core.io.Resource;

/**
 * Custom SSL HttpClientFactoy.
 * It allow to specify the certificate for a single specific implementation.
 * It's needed when you have a single URL to call but different certificate, each one specific for a single page/function/user
 * 
 * @author roberto.gabrieli
 *
 */
public class CustomSSLHttpClientFactory implements FactoryBean<HttpClient>
{
    protected Resource keyStoreFile;

    protected String   keyStorePassword;

    protected String   keyStoreType;

    protected Resource trustStoreFile;

    protected String   trustStorePassword;

    protected String[] allowedProtocols;

    protected String   certAlias;

    public CustomSSLHttpClientFactory()
    {

    }

    /**
     * Contructor for factory-bean
     * 
     * @param keyStoreFile org.springframework.core.io.Resource to specify the keystore
     * @param keyStorePassword 
     * @param keyStoreType if null default JKS will be used 
     * @param trustStoreFile
     * @param trustStorePassword
     * @param allowedProtocols authentication protocols
     * @param certAlias the client certificate alias. If null default behavior 
     */
    public CustomSSLHttpClientFactory(Resource keyStoreFile,
                               String keyStorePassword,
                               String keyStoreType,
                               Resource trustStoreFile,
                               String trustStorePassword,
                               String[] allowedProtocols,
                               String certAlias)
    {
        super();
        this.keyStoreFile = keyStoreFile;
        this.keyStorePassword = keyStorePassword;
        this.keyStoreType = keyStoreType;
        this.trustStoreFile = trustStoreFile;
        this.trustStorePassword = trustStorePassword;
        this.allowedProtocols = allowedProtocols;
        this.certAlias = certAlias;
    }

    /**
     * Little trick to pass over some stupid contentLength error
     * 
     * @author roberto.gabrieli
     */
    private class ContentLengthHeaderRemover implements HttpRequestInterceptor
    {
        @Override
        public void process(HttpRequest request,
                            HttpContext context) throws HttpException, IOException
        {
            request.removeHeaders(HTTP.CONTENT_LEN);// fighting org.apache.http.protocol.RequestContent's ProtocolException("Content-Length header already present");
        }
    }


    /**
     * Private class to hack the certificate alias choice.
     * 
     * @author roberto.gabrieli
     *
     */
    private class AliasPrivateKeyStrategy implements PrivateKeyStrategy
    {
        private String alias;

        public AliasPrivateKeyStrategy(String alias)
        {
            this.alias = alias;
        }

        /**
         * This metod return the alias name specified in the constructor.
         */
        public String chooseAlias(Map<String, PrivateKeyDetails> aliases,
                                  Socket socket)
        {
            return alias;
        }

    }

    /**
     * Method that return a CloseableHttpClient
     * 
     */
    public CloseableHttpClient getObject() throws Exception
    {
        KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
        KeyStore keyStore = KeyStore.getInstance(this.keyStoreType != null ? this.keyStoreType : KeyStore.getDefaultType());
        InputStream instreamTrust = trustStoreFile.getInputStream();
        InputStream instreamKeys = keyStoreFile.getInputStream();

        //Load of KEYSTORE and TRUSTSTORE
        try
        {
            trustStore.load(instreamTrust, trustStorePassword.toCharArray());
            keyStore.load(instreamKeys, keyStorePassword.toCharArray());
        }
        finally
        {
            instreamKeys.close();
            instreamTrust.close();
        }

        SSLContextBuilder sslCtxBuilder = SSLContexts.custom().loadTrustMaterial(trustStore, new TrustSelfSignedStrategy());

        PrivateKeyStrategy apks = null;
        // check if the alias is specified null and "" will mean -no alias-
        if ( this.certAlias != null && !this.certAlias.trim().equals("") )
        {
            apks = new AliasPrivateKeyStrategy(this.certAlias);
            sslCtxBuilder = sslCtxBuilder.loadKeyMaterial(keyStore, keyStorePassword.toCharArray(), apks);
        }
        else
        {
            sslCtxBuilder = sslCtxBuilder.loadKeyMaterial(keyStore, keyStorePassword.toCharArray());
        }
        SSLContext sslcontext = sslCtxBuilder.build();

        //All the stuff for the connection build
        HttpClientBuilder builder = HttpClientBuilder.create();
        SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslcontext, allowedProtocols, null, SSLConnectionSocketFactory.BROWSER_COMPATIBLE_HOSTNAME_VERIFIER);

        builder.setSSLSocketFactory(sslsf);
        Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory> create().register("https", sslsf).register("http", new PlainConnectionSocketFactory()).build();
        HttpClientConnectionManager ccm = new BasicHttpClientConnectionManager(registry);
        builder.setConnectionManager(ccm);
        CloseableHttpClient httpclient = builder.build();

        return httpclient;
    }

    public Class<?> getObjectType()
    {
        return HttpClient.class;
    }

    public boolean isSingleton()
    {
        return false;
    }

}

这是"spring-config.xml"

中需要的配置
<!-- Usual settings for WebServiceTemplate
<bean id="messageFactory" class="org.springframework.ws.soap.saaj.SaajSoapMessageFactory" />

<bean id="marshaller" class="org.springframework.oxm.jaxb.Jaxb2Marshaller">
    <property name="contextPaths">
        <list>
            <value>foo.bar.model.jaxb</value>
        </list>
    </property>
</bean>

<!-- The bean that will do the magic! -->
<bean id="CustomSSLHttpClientFactoryFactory" class="foo.bar.services.CustomSSLHttpClientFactoryFactory" />


<!-- Bean that consume the WebService -->
<bean id="myBusinessLogicBean" class="foo.bar.services.MyBusinessLogicBean">
    <property name="webServiceTemplate">
        <bean id="webServiceTemplate" class="org.springframework.ws.client.core.WebServiceTemplate">
            <constructor-arg ref="messageFactory" />
            <property name="messageSender">
                <bean id="modifiedHttpComponentsMessageSender"
                    class="org.springframework.ws.transport.http.HttpComponentsMessageSender">
                    <property name="httpClient">
                        <bean factory-bean="customSSLHttpClient" class="it.volkswagen.arch.services.security.CustomSSLHttpClientFactory" >
                            <constructor-arg name="keyStoreFile" value="file://myPath/keystore.jks" />
                            <constructor-arg name="keyStorePassword" value="myKeyStorePwd" />
                            <constructor-arg name="trustStoreFile" value="file://myPath/truststore.jks" />
                            <constructor-arg name="trustStorePassword" value="myTrustStorePwd" />
                            <constructor-arg name="keyStoreType" value="JKS" />
                            <constructor-arg name="allowedProtocols">
                                <array>
                                    <value>TLSv1</value>
                                </array>
                            </constructor-arg>
                            <constructor-arg name="certAlias" value="site_a"/>
                        </bean>
                    </property>
                </bean>
            </property>

            <property name="marshaller" ref="marshaller" />
            <property name="unmarshaller" ref="marshaller" />
            <property name="defaultUri"
                value="http://foo.bar/ws-demo/myConsumedWs" />
        </bean>
    </property>
</bean>

我无法通过所有身份验证来模拟 Web 服务,因此为了对我的工厂进行一些测试,我必须在 IIS 8.5 中部署两个带有 SSL 客户端证书身份验证的小站点和一个 java 主站点class 来源:

package foo.bar.runnable;

import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;

import foo.bar.services.CustomSSLHttpClientFactory;

public class RunTestHttpClient
{
    private static String   urlSitoA           = "https://nbk196.addvalue.it";

    private static String   urlSitoB           = "https://nbk196b.addvalue.it";

    private static String   trustStoreFilePath = "truststore.jks";

    private static String   trustStorePassword = "P@ssw0rd";

    private static String[] allowedProtocols   =
                                               { "TLSv1" };

    public static void main(String[] args)
    {
        System.out.println("########## Test multy call with different cert in same keystore #############");
        System.out.println(" ----- ----- CASE OK ----- ----- ");
        testLogic("keystore.jks", "keystore.jks", "P@ssw0rd", null, "site_a", "site_b");
        System.out.println(" ----- ----- CASE KO ----- ----- ");
        System.out.println("########## Test multy call with different keystore #############");
        System.out.println(" ----- ----- CASE OK ----- ----- ");
        testLogic("site_a.pfx", "site_b.pfx", "P@ssw0rd", "pkcs12", null, null);
        System.out.println(" ----- ----- CASE KO ----- ----- ");
        testLogic("site_b.pfx", "site_a.pfx", "P@ssw0rd", "pkcs12", null, null);
    }

    private static void testLogic(String keyStoreFilePathA,
                                  String keyStoreFilePathB,
                                  String keyStorePassword,
                                  String keyStoreType,
                                  String certAliasSitoA,
                                  String certAliasSitoB)
    {
        Resource keyStoreFileA = new ClassPathResource(keyStoreFilePathA);
        Resource keyStoreFileB = new ClassPathResource(keyStoreFilePathB);

        Resource trustStoreFile = new ClassPathResource(trustStoreFilePath);

        CustomSSLHttpClientFactory clientFactorySitoA = new CustomSSLHttpClientFactory(keyStoreFileA, keyStorePassword, keyStoreType, trustStoreFile, trustStorePassword, allowedProtocols, certAliasSitoA);
        CustomSSLHttpClientFactory clientFactorySitoB = new CustomSSLHttpClientFactory(keyStoreFileB, keyStorePassword, keyStoreType, trustStoreFile, trustStorePassword, allowedProtocols, certAliasSitoB);

        try
        {
            CloseableHttpClient httpClientSitoA = clientFactorySitoA.getObject();

            HttpGet httpgetSitoA = new HttpGet(urlSitoA);

            try (CloseableHttpResponse responseSitoA = httpClientSitoA.execute(httpgetSitoA))
            {
                HttpEntity entitySitoA = responseSitoA.getEntity();

                System.out.println("------------------ SitoA ----------------------");
                System.out.println(responseSitoA.getStatusLine());
                if ( entitySitoA != null )
                {
                    System.out.println("Response content length: " + entitySitoA.getContentLength());
                    System.out.printf(EntityUtils.toString(entitySitoA));
                }
                EntityUtils.consume(entitySitoA);
            }

            System.out.println();
        }
        catch ( Exception e )
        {
            e.printStackTrace(System.out);
        }

        try
        {
            CloseableHttpClient httpClientSitoB = clientFactorySitoB.getObject();

            HttpGet httpgetSitoB = new HttpGet(urlSitoB);

            try (CloseableHttpResponse responseSitoB = httpClientSitoB.execute(httpgetSitoB))
            {
                HttpEntity entitySitoB = responseSitoB.getEntity();

                System.out.println("------------------ SitoB ----------------------");
                System.out.println(responseSitoB.getStatusLine());
                if ( entitySitoB != null )
                {
                    System.out.println("Response content length: " + entitySitoB.getContentLength());
                    System.out.printf(EntityUtils.toString(entitySitoB));
                }
                EntityUtils.consume(entitySitoB);
            }
            System.out.println();
        }
        catch ( Exception e )
        {
            e.printStackTrace(System.out);
        }
    }
}

这是控制台输出:

########## Test multy call with different cert in same keystore #############
 ----- ----- CASE OK ----- ----- 
------------------ SitoA ----------------------
HTTP/1.1 200 OK
Response content length: -1
<html>
<head></head>
<body>CARICATO SITO A</body>
</html>
------------------ SitoB ----------------------
HTTP/1.1 200 OK
Response content length: -1
<html>
<head></head>
<body>CARICATO SITO B</body>
</html>
 ----- ----- CASE KO ----- ----- 
------------------ SitoA ----------------------
HTTP/1.1 401 Unauthorized
Response content length: 6319
java.util.UnknownFormatConversionException: Conversion = ';'
    at java.util.Formatter.checkText(Formatter.java:2547)
    at java.util.Formatter.parse(Formatter.java:2523)
    at java.util.Formatter.format(Formatter.java:2469)
    at java.io.PrintStream.format(PrintStream.java:970)
    at java.io.PrintStream.printf(PrintStream.java:871)
    at foo.bar.runnable.RunTestHttpClient.testLogic(RunTestHttpClient.java:70)
    at foo.bar.runnable.RunTestHttpClient.main(RunTestHttpClient.java:32)
------------------ SitoB ----------------------
HTTP/1.1 401 Unauthorized
Response content length: 6320
java.util.UnknownFormatConversionException: Conversion = ';'
    at java.util.Formatter.checkText(Formatter.java:2547)
    at java.util.Formatter.parse(Formatter.java:2523)
    at java.util.Formatter.format(Formatter.java:2469)
    at java.io.PrintStream.format(PrintStream.java:970)
    at java.io.PrintStream.printf(PrintStream.java:871)
    at foo.bar.runnable.RunTestHttpClient.testLogic(RunTestHttpClient.java:97)
    at foo.bar.runnable.RunTestHttpClient.main(RunTestHttpClient.java:32)
########## Test multy call with different keystore #############
 ----- ----- CASE OK ----- ----- 
------------------ SitoA ----------------------
HTTP/1.1 200 OK
Response content length: -1
<html>
<head></head>
<body>CARICATO SITO A</body>
</html>
------------------ SitoB ----------------------
HTTP/1.1 200 OK
Response content length: -1
<html>
<head></head>
<body>CARICATO SITO B</body>
</html>
 ----- ----- CASE KO ----- ----- 
------------------ SitoA ----------------------
HTTP/1.1 401 Unauthorized
Response content length: 6319
java.util.UnknownFormatConversionException: Conversion = ';'
    at java.util.Formatter.checkText(Formatter.java:2547)
    at java.util.Formatter.parse(Formatter.java:2523)
    at java.util.Formatter.format(Formatter.java:2469)
    at java.io.PrintStream.format(PrintStream.java:970)
    at java.io.PrintStream.printf(PrintStream.java:871)
    at foo.bar.runnable.RunTestHttpClient.testLogic(RunTestHttpClient.java:70)
    at foo.bar.runnable.RunTestHttpClient.main(RunTestHttpClient.java:37)
------------------ SitoB ----------------------
HTTP/1.1 401 Unauthorized
Response content length: 6320
java.util.UnknownFormatConversionException: Conversion = ';'
    at java.util.Formatter.checkText(Formatter.java:2547)
    at java.util.Formatter.parse(Formatter.java:2523)
    at java.util.Formatter.format(Formatter.java:2469)
    at java.io.PrintStream.format(PrintStream.java:970)
    at java.io.PrintStream.printf(PrintStream.java:871)
    at foo.bar.runnable.RunTestHttpClient.testLogic(RunTestHttpClient.java:97)
    at foo.bar.runnable.RunTestHttpClient.main(RunTestHttpClient.java:37)

我在通过 SSL 调用 WS 时遇到了类似的问题。我在我的项目中使用了 Spring-webservice。

在组件 class 下面创建,它在启动时将 ssl 证书和密码加载到系统变量中,仅此而已。希望对以后的人有所帮助。

@Component
public class WsVariableLoader
{
  @Value("${keystore.filename}")
  private String keystoreFilename;

  @Value("${keystore.password}")
  private String keystorePassword;

  @PostConstruct
  public void init()
  {
    System.setProperty("javax.net.ssl.trustStore", keystoreFilename);
    System.setProperty("javax.net.ssl.trustStorePassword", keystorePassword);
    System.setProperty("javax.net.ssl.keyStore", keystoreFilename);
    System.setProperty("javax.net.ssl.keyStorePassword", keystorePassword);
}

}

有一种更简单的方法,而不是使用手动设置 SSL 上下文并使用拦截器删除内容长度的自定义 HTTP 客户端工厂 bean headers(如果你问我的话,那就是小 Hokie)。

Spring 有一个 HttpsUrlConnectionMessageSender,它会自动正确设置 SSLContext,并允许您通过 KeyStoreManager 和 TrustStoreManager 指定不同的密钥库和信任库。这种方法使从客户端进行相互 SSL 身份验证变得更加清晰。

public class MyWebServiceClient extends WebServiceGatewaySupport implements MyWebServicePortType {

@Configuration
public static class MyClientConfig {
    @Value("${myws.endpoint.url}")
    private String url;

    @Value("${myws.keystore}")
    private Resource keyStore;
    @Value("${myws.keystore.password}")
    private String keyStorePass;
    @Value("${myws.truststore}")
    private Resource trustStore;
    @Value("${myws.truststore.password}")
    private String trustStorePass;

    @Bean 
    public Jaxb2Marshaller myWebServiceClientMarshaller() {
        Jaxb2Marshaller marshaller = new Jaxb2Marshaller();
        marshaller.setContextPath("com.myws.types");
        return marshaller;
    }

    @Bean
    public MyWebServiceClient myWebServiceClient() throws Exception {
        MyWebServiceClient client = new MyWebServiceClient();
        client.setDefaultUri(this.url);
        client.setMarshaller(myWebServiceClientMarshaller());
        client.setUnmarshaller(myWebServiceClientMarshaller());

        KeyStore ks = KeyStore.getInstance("JKS");
        ks.load(keyStore.getInputStream(), keyStorePass.toCharArray());
        logger.info("Loaded keyStore: "+keyStore.getURI().toString());
        try { keyStore.getInputStream().close(); } catch(IOException e) {}
        KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
        keyManagerFactory.init(ks, keyStorePass.toCharArray());

        KeyStore ts = KeyStore.getInstance("JKS");
        ts.load(trustStore.getInputStream(), trustStorePass.toCharArray());
        logger.info("Loaded trustStore: "+trustStore.getURI().toString());
        try { trustStore.getInputStream().close(); } catch(IOException e) {}
        TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
        trustManagerFactory.init(ts);

        HttpsUrlConnectionMessageSender msgSender = new HttpsUrlConnectionMessageSender();
        msgSender.setKeyManagers(keyManagerFactory.getKeyManagers());
        msgSender.setTrustManagers(trustManagerFactory.getTrustManagers());

        client.setMessageSender(msgSender);

        return client;
    }


    // client port method implementations ...
    public MyOperationResponse processMyOperation(MyOperationRequest request) {
        return (MyOperationResponse) getWebServiceTemplate().marshalSendAndReceive(request, new SoapActionCallback("urn:ProcessMyOperation"));
    }

}