Java で RESTをなげるサービスの開発をしていて、自分がなげているリクエストはあってるかな?って確認したいケースがあります。
そんなときはプロキシを通すわけですが、そのやりかたを調べたことがあって、その結果を整理しておきます。
JavaのREST Clientとしては JAX-RSのリファレンス実装にもなっている Jerseyのクライアント部分を用いています。
先人の知恵にはいつも感謝ですね。基本的にはこのサイトの情報を参考にさせていただきました。
JerseyClientでプロキシを通す方法
なのですが、すくなくとも今回アクセスしているAPIではSSLの下記のエラー
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 の設定
<!-- だた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で。
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
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でプロキシを通す方法
Jersey 2.25 User Guide/5.9. Securing a Client
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証明書の検証処理の無効化、ホスト名検証処理の無効化をする下記のコードを挿入します。
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でプロキシを通す方法
Jersey 2.25 User Guide/5. Client API
Jersey 2.25 User Guide/5.9. Securing a Client