JavaでSSLでRESTを投げるときに、プロキシを通す方法



Java で RESTをなげるサービスの開発をしていて、自分がなげているリクエストはあってるかな？って確認したいケースがあります。
そんなときはプロキシを通すわけですが、そのやりかたを調べたことがあって、その結果を整理しておきます。
JavaのREST Clientとしては JAX-RSのリファレンス実装にもなっている [Jersey](https://jersey.java.net/)のクライアント部分を用いています。

先人の知恵にはいつも感謝ですね。基本的にはこのサイトの情報を参考にさせていただきました。
[JerseyClientでプロキシを通す方法](http://qiita.com/opengl-8080/items/ca33230cb8df1148fb6f)

なのですが、すくなくとも今回アクセスしているAPIではSSLの下記のエラー

```java
javax.ws.rs.ProcessingException: javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException:
 PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: 
 unable to find valid certification path to requested target
```

が発生してしまうようで、追加で調査したところを中心にまとめます。


## pom.xml の設定

```xml:pom.xml
<!-- だたJersey のクライアントをつかうだけなら -->
<dependency>
  <groupId>org.glassfish.jersey.core</groupId>
  <artifactId>jersey-client</artifactId>
  <version>2.25</version>
</dependency>

<!-- さらにJerseyでProxyを使う場合。 -->
<dependency>
  <groupId>org.glassfish.jersey.connectors</groupId>
  <artifactId>jersey-apache-connector</artifactId>
  <version>2.25</version>
</dependency>
```


## クライアントの構築

アクセス先はQiitaのAPI

```
https://qiita.com/api/v2/users/qiita/items?per_page=1&page=4
```

を対象にしてみます。

###まずはcurlで。

```bash
curl 'https://qiita.com/api/v2/users/qiita/items' -d 'per_page=1' -d 'page=4' -G
```

なにかJSONが返ってきたと思います。

###Javaで
つぎにJavaです。まずはプロキシを通さないパターンです。

下記のドキュメントを参考に実装していきます。
[Jersey 2.25 User Guide/5. Client API](https://jersey.java.net/documentation/latest/user-guide.html#d0e4812)



```java:ClientSamples.java

import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

public class ClientSamples {

    public static void main(String[] args) {
        new ClientSamples().execute();
    }

    public void execute() {
        String target = "https://qiita.com";
        String path = "/api/v2/users/qiita/items";

        // Client client = createSecureClient();
        Client client = createClient();
        Response restResponse = client
                .target(target)
                .path(path)
                .queryParam("page", "4")
                .queryParam("per_page", "1")
                .request(MediaType.APPLICATION_JSON_TYPE)
                .get();

        System.out.println(restResponse.readEntity(String.class));
    }

    private Client createClient() {
        return ClientBuilder.newClient();
    }

}
```

curlの場合と、おなじ結果が得られるはずです。


###プロキシを通してみる。
下記のページならびに先ほど参考にさせていただきましたと書いたサイトを参考に。
[JerseyClientでプロキシを通す方法](http://qiita.com/opengl-8080/items/ca33230cb8df1148fb6f)
[Jersey 2.25 User Guide/5.9. Securing a Client](https://jersey.java.net/documentation/latest/user-guide.html#d0e5228)


```java:ClientSamples.java
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

import org.glassfish.jersey.apache.connector.ApacheConnectorProvider;
import org.glassfish.jersey.client.ClientConfig;
import org.glassfish.jersey.client.ClientProperties;

public class ClientSamples {

    public static void main(String[] args) {
        new ClientSamples().execute();
    }

    public void execute() {
        String target = "https://qiita.com";
        String path = "/api/v2/users/qiita/items";

        Client client = createSecureClient();
        // Client client = createClient();
        Response restResponse = client
                .target(target)
                .path(path)
                .queryParam("page", "4")
                .queryParam("per_page", "1")
                .request(MediaType.APPLICATION_JSON_TYPE).get();

        System.out.println(restResponse.readEntity(String.class));
    }

    private Client createSecureClient() {
        String proxyHost = "http://127.0.0.1:8080";
        ClientConfig config = new ClientConfig();

        // providerをproxy対応?にする
        config.connectorProvider(new ApacheConnectorProvider());
        config.property(ClientProperties.PROXY_URI, proxyHost);
        // config.property(ClientProperties.PROXY_USERNAME, "userName");
        // config.property(ClientProperties.PROXY_PASSWORD, "password");

        // builderの生成
        ClientBuilder b = ClientBuilder.newBuilder().withConfig(config);
        return b.build();
    }

}

```


実行したところやはり、、javax.net.ssl.SSLHandshakeException が発生します。プロキシを通したことで、信頼できないSSLサーバに接続しようとしているように見えているっぽいですね。なので、SSL証明書の検証処理の無効化、ホスト名検証処理の無効化をする下記のコードを挿入します。

```java:ClientSamples.java
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.X509TrustManager;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

import org.glassfish.jersey.apache.connector.ApacheConnectorProvider;
import org.glassfish.jersey.client.ClientConfig;
import org.glassfish.jersey.client.ClientProperties;


public class ClientSamples {

    public static void main(String[] args) {
        new ClientSamples().execute();
    }

    public void execute() {
        String target = "https://qiita.com";
        String path = "/api/v2/users/qiita/items";

        Client client = createSecureClient();
        // Client client = createClient();
        Response restResponse = client
                .target(target)
                .path(path)
                .queryParam("page", "4")
                .queryParam("per_page", "1")
                .request(MediaType.APPLICATION_JSON_TYPE)
                .get();

        System.out.println(restResponse.readEntity(String.class));
    }

    private Client createClient() {
        return ClientBuilder.newClient();
    }

    private Client createSecureClient() {
        String proxyHost = "http://127.0.0.1:8080";
        ClientConfig config = new ClientConfig();

        // providerをproxy対応?にする
        config.connectorProvider(new ApacheConnectorProvider());
        config.property(ClientProperties.PROXY_URI, proxyHost);
        // config.property(ClientProperties.PROXY_USERNAME, "userName");
        // config.property(ClientProperties.PROXY_PASSWORD, "password");

        SSLContext sslContext = createSSLContext();
        HostnameVerifier hostnameVerifier = createHostNameVerifier();

        // builderの生成
        ClientBuilder b = ClientBuilder.newBuilder().withConfig(config)
                .sslContext(sslContext).hostnameVerifier(hostnameVerifier);
        return b.build();
    }

    private SSLContext createSSLContext() {
        SSLContext sslContext = null;
        try {
            sslContext = SSLContext.getInstance("TLS");
            sslContext.init(null,
                    new X509TrustManager[] { new X509TrustManager() {
                        @Override
                        public void checkClientTrusted(X509Certificate[] chain,String authType) throws CertificateException {}
                        @Override
                        public void checkServerTrusted(X509Certificate[] chain,String authType) throws CertificateException {}
                        @Override
                        public X509Certificate[] getAcceptedIssuers() {return new X509Certificate[0];}
                    } }, new SecureRandom());
            // HttpsURLConnection
            // .setDefaultSSLSocketFactory(sslContext.getSocketFactory());

        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (KeyManagementException e) {
            e.printStackTrace();
        }
        return sslContext;
    }

    private HostnameVerifier createHostNameVerifier() {
        return new HostnameVerifier() {
            @Override
            public boolean verify(String hostname, SSLSession session) {return true;}
        };
    }
}
```

これでうまくいくようです。。テストサーバにオレオレ証明書を入れているケースなどにも対応できそうですね。

##関連リンク
[JerseyClientでプロキシを通す方法](http://qiita.com/opengl-8080/items/ca33230cb8df1148fb6f)
[Jersey 2.25 User Guide/5. Client API](https://jersey.java.net/documentation/latest/user-guide.html#d0e4812)
[Jersey 2.25 User Guide/5.9. Securing a Client](https://jersey.java.net/documentation/latest/user-guide.html#d0e5228)