KCF Labo Blog

KDDI Commerce Forwardの開発ブログ

Java × Spring Boot のlocal環境でのProxy設定とSSL証明書の無視

Java × Spring Boot のこと

はじめまして、エンジニアの坂本です。
私たちのチームでは Java × Spring Boot で開発を行なっています。
Spring Boot いいですね。

WEBアプリケーション開発で今までJavaが敬遠されて来た理由のほとんど (設定周りの複雑さなどから来る敷居のたかさ)
が解消されていると私は感じています。

ところで、普段の開発では
「こういうことがしたい」ということが明確なのに「どうやったらいいんだろう?」という事がよく起こりますよね。

そういう時にはとにかく「検索」することで、インターネットにある同じ疑問や困難にあたって解決して来た様ざまな人たちの記事に、私は助けられています。

そこで今回は、少しは恩返し・貢献したいという思いから、この記事を書いてます、 よろしくお願いします。

Spring Bootのlocal環境でのHTTP/HTTPSのProxy設定

WEBアプリケーション開発では「外部のAPIに接続する」という必要がよくあります。

しかしlocal環境の開発では(本番環境やステージング環境と違って)社内などの決まったProxyを通して接続しなければいけない、 という状況がよくあると思います。

今回はそんな場合のお話です。

まずは結論のプログラム・ソースコードを載せますね。

package jp.samplesample.application.configuration;

import org.apache.http.conn.ssl.X509HostnameVerifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.context.annotation.PropertySource;

import javax.annotation.PostConstruct;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;

import java.io.IOException;
import java.net.Authenticator;
import java.net.PasswordAuthentication;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;

/**
 * local環境の時だけHTTP/HTTPSでプロキシーを通るようにしてHTTP接続先のSSL証明書チェックの無効化を設定します。
 */
@Configuration
@Profile("local")
@PropertySource("classpath:proxy.properties")
public class HttpProxyConfiguration {

    @Value("${proxy.host}")
    private String host;

    @Value("${proxy.port}")
    private String port;

    @Value("${proxy.user}")
    private String user;

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

    @PostConstruct
    private void setProxyAndDisableSSLValidation() {
        // Proxy for System
        System.setProperty("jdk.http.auth.tunneling.disabledSchemes", "");
        System.setProperty("jdk.http.auth.proxying.disabledSchemes", "");
        System.setProperty("http.proxyHost", host);
        System.setProperty("http.proxyPort", port);
        System.setProperty("https.proxyHost", host);
        System.setProperty("https.proxyPort", port);
        Authenticator.setDefault(new Authenticator() {
            @Override
            protected PasswordAuthentication getPasswordAuthentication() {
                return new PasswordAuthentication(user, password.toCharArray());
            }
        });
        // SSL証明書チェックの無効化
        disableSSLValidation();
    }

    // local環境用のSSLContextとその初期化
    private SSLContext localSSLContext() {
        try {
            final SSLContext sslContext = SSLContext.getInstance("TLS");
            sslContext.init(null, new TrustManager[]{new X509TrustManager() {
                @Override
                public void checkClientTrusted(X509Certificate[] certs, String s) throws CertificateException {
                }
                @Override
                public void checkServerTrusted(X509Certificate[] certs, String s) throws CertificateException {
                }
                @Override
                public X509Certificate[] getAcceptedIssuers() {
                    return new X509Certificate[0];
                }
            }}, null);
            return sslContext;
        } catch (NoSuchAlgorithmException | KeyManagementException e) {
            throw new RuntimeException("sslContext init failed.", e);
        }
    }

    // local環境用のHostnameVerifier
    private X509HostnameVerifier localHostnameVerifier() {
        return new X509HostnameVerifier() {
            @Override
            public void verify(String hostname, SSLSocket ssl) throws IOException {
            }
            @Override
            public void verify(String hostname, X509Certificate cert) throws SSLException {
            }
            @Override
            public void verify(String hostname, String[] cns, String[] subjectAlts) throws SSLException {
            }
            @Override
            public boolean verify(String hostname, SSLSession session) {
                return true;
            }
        };
    }

    // HTTPS接続先のSSL証明書チェックの無効化
    private void disableSSLValidation() {
        HttpsURLConnection.setDefaultSSLSocketFactory(localSSLContext().getSocketFactory());
        HttpsURLConnection.setDefaultHostnameVerifier(localHostnameVerifier());
    }

}

このプログラム・ソースコードのクラスの内容ですが、
まずは内から外へのHTTP/HTTPS通信に関してProxy設定をしています。

しかし、それだけではHTTPS通信は接続先の証明書チェックで引っ掛かるので、証明書チェックを無効化しています。

クラスの先頭に付いている
@Configuraion@Profile("local")
アノテーションが重要です。

@Configurationは設定クラスですよ、
@Profile("local")はlocal環境だけの設定ですよ、
という意味ですね。

このConfigurationクラスを入れておくだけで、local環境の実行においては、どこで通信してもProxy設定が効いているはずです。


おっと「proxy.properties」の説明が抜けていました。

proxy.host=(Proxyのホスト名)
proxy.port=(Proxyのポート番号)
proxy.user=(Proxyにおける自分のユーザー名)
proxy.password=(Proxyにおける自分のパスワード)

上記の内容を書いたテキストを「proxy.properties」というファイル名で保存して、
Spring Boot から見えるリソースとして、
プロジェクト・フォルダの中の /src/main/resources の直下などに置いてください。

RestTemplateの場合はProxy設定が効いている

さて、具体的なAPI通信のほうですが、例えば
普通に直接的に「RestTemplate」を使う場合などは、上記のクラスの設定が入っているだけで大丈夫です。

RestTemplate restTemplate = new RestTemplate();
XXX xxx = restTemplate.getForObject("https://samplesample.jp/api/12345", XXX.class);

よくあるこんな感じですね。

RestTemplateBuilder・RestTemplateCustomizerを使う場合

しかし「RestTemplateBuilder・RestTemplateCustomizer」を使う場合には注意が必要です。

qiita.com

この有益なサイトのRestTemplateに関する記事にある
「3rdパーティ製ライブラリとの連携」の項目に書いてあるように
複数のHTTPクライアントのライブラリがクラスパス上にあった場合には

Java標準のHttpURLConnection (デフォルト)

の検出される優先順位が一番最後だからです。

@Override
public void customize(RestTemplate restTemplate) {
    new RestTemplateBuilder()
        .requestFactory(SimpleClientHttpRequestFactory.class)
        .configure(restTemplate);
}

上記のように私たちのチームでは
Java標準の実装を使うSimpleClientHttpRequestFactoryクラスをrequestFactoryに指定しています。

この指定をしない場合には「HTTPS通信の接続先のSSL証明書チェックの無効化」でコケたと思います。

付録:OpenId4JavaのProxy設定

ここから以下は、非常に限られた用途の場合なので、
「必要ない」という方は読み飛ばしてください。

OpenID認証のクライアントのためのJavaライブラリに
OpenId4Java
というものがあります。

github.com

今わざわざこれを選ぶという人がどれだけいるのか分かりませんが、
私たちのチームは今回は様ざまな事情から、
このライブラリを使う必要がありました。

そしてここでもlocal開発環境でのProxy設定の問題にあたったわけです。

色いろなインターネットのサイトなどを検索して断片などを調べながら、
最後にいま落ち着いているプログラム・ソースコードの状態は、
以下のような感じです。

package jp.samplesample.application.configuration;

import java.io.IOException;
import java.net.Authenticator;
import java.net.PasswordAuthentication;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;

import javax.annotation.PostConstruct;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;

import org.apache.http.conn.ssl.X509HostnameVerifier;
import org.openid4java.consumer.ConsumerManager;
import org.openid4java.discovery.Discovery;
import org.openid4java.discovery.yadis.YadisResolver;
import org.openid4java.server.RealmVerifierFactory;
import org.openid4java.util.HttpClientFactory;
import org.openid4java.util.HttpFetcherFactory;
import org.openid4java.util.ProxyProperties;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.context.annotation.PropertySource;

/**
 * local環境の時だけHTTP/HTTPSでプロキシーを通るようにしてHTTP接続先のSSL証明書チェックの無効化を設定します。<br>
 * あわせてlocal環境用のOpenId4Java向けのプロキシー設定とConsumerManagerの設定もここで行います。
 */
@Configuration
@Profile("local")
@PropertySource("classpath:proxy.properties")
public class HttpProxyConfiguration {

    @Value("${proxy.host}")
    private String host;

    @Value("${proxy.port}")
    private String port;

    @Value("${proxy.user}")
    private String user;

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

    @PostConstruct
    private void setProxyAndDisableSSLValidation() {
        // Proxy for System
        System.setProperty("jdk.http.auth.tunneling.disabledSchemes", "");
        System.setProperty("jdk.http.auth.proxying.disabledSchemes", "");
        System.setProperty("http.proxyHost", host);
        System.setProperty("http.proxyPort", port);
        System.setProperty("https.proxyHost", host);
        System.setProperty("https.proxyPort", port);
        Authenticator.setDefault(new Authenticator() {
            @Override
            protected PasswordAuthentication getPasswordAuthentication() {
                return new PasswordAuthentication(user, password.toCharArray());
            }
        });
        // Proxy for OpenId4Java
        ProxyProperties proxyProperties = new ProxyProperties();
        proxyProperties.setProxyHostName(host);
        proxyProperties.setProxyPort(Integer.parseInt(port));
        proxyProperties.setUserName(user);
        proxyProperties.setPassword(password);
        HttpClientFactory.setProxyProperties(proxyProperties);
        // SSL証明書チェックの無効化
        disableSSLValidation();
    }

    // local環境用のSSLContextとその初期化
    private SSLContext localSSLContext() {
        try {
            final SSLContext sslContext = SSLContext.getInstance("TLS");
            sslContext.init(null, new TrustManager[]{new X509TrustManager() {
                @Override
                public void checkClientTrusted(X509Certificate[] certs, String s) throws CertificateException {
                }
                @Override
                public void checkServerTrusted(X509Certificate[] certs, String s) throws CertificateException {
                }
                @Override
                public X509Certificate[] getAcceptedIssuers() {
                    return new X509Certificate[0];
                }
            }}, null);
            return sslContext;
        } catch (NoSuchAlgorithmException | KeyManagementException e) {
            throw new RuntimeException("sslContext init failed.", e);
        }
    }

    // local環境用のHostnameVerifier
    private X509HostnameVerifier localHostnameVerifier() {
        return new X509HostnameVerifier() {
            @Override
            public void verify(String hostname, SSLSocket ssl) throws IOException {
            }
            @Override
            public void verify(String hostname, X509Certificate cert) throws SSLException {
            }
            @Override
            public void verify(String hostname, String[] cns, String[] subjectAlts) throws SSLException {
            }
            @Override
            public boolean verify(String hostname, SSLSession session) {
                return true;
            }
        };
    }

    // HTTPS接続先のSSL証明書チェックの無効化
    private void disableSSLValidation() {
        HttpsURLConnection.setDefaultSSLSocketFactory(localSSLContext().getSocketFactory());
        HttpsURLConnection.setDefaultHostnameVerifier(localHostnameVerifier());
    }

    // OpenId4Java
    // local環境のみ
    @Bean
    @Qualifier("consumerManager")
    public ConsumerManager localConsumerManager() {
        Discovery discovery = new Discovery();
        discovery.setYadisResolver(
                    new YadisResolver(new HttpFetcherFactory(localSSLContext(), localHostnameVerifier())));
        return new ConsumerManager(
                    new RealmVerifierFactory(
                    new YadisResolver(new HttpFetcherFactory(localSSLContext(), localHostnameVerifier()))),
                    discovery,
                    new HttpFetcherFactory(localSSLContext(), localHostnameVerifier()));
    }

}


もちろんlocal環境以外の場合には、
OpenId4JavaのそのままのConsumerManagerを使わないといけないので、
以下の設定クラスを入れています。

package jp.samplesample.application.configuration;

import org.openid4java.consumer.ConsumerManager;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;

@Configuration
public class OpenId4JavaConfiguration {

    // local環境以外
    @Bean
    @Profile("!local")
    @Qualifier("consumerManager")
    public ConsumerManager consumerManager() {
        return new ConsumerManager();
    }

    // local環境用のConsumerManagerの設定はHttpProxyConfigurationクラスに宣言してあります

}