2016年末のAndroidでのSSL対応に関して

はじめに

この記事は Android その3 Advent Calendar 2016 - Qiita

の六日目の記事です。


近々の状況

2016中にApp Storeに提出されるすべてのアプリを「App Transport Security(ATS)」に対応し、有効化することを必須条件にする

みたいなセキュリティ強化的な話から、今年の後半辺りからサーバーサイド含めてSSL対応を急遽する

みたいな企業が増えている状況かと思います。

もちろん iOS とは別サーバー/別APポイントAndroid がhttpのサーバーを使い続けるみたいな形はありえないので、a側も頑張らないとダメという形になります

去年の Android 6.0対応あたりで

  • target-23 以降でビルドする場合、
    • AndroidHttpClientが廃止
    • 使いたければ http.legacy 指定をしてください
android {
    useLibrary 'org.apache.http.legacy'
}

とは書いていましたが、なんかコレ使うの嫌だよね

みたいな流れで、 HttpURLConnection ベースの処理に書き換えていました

Okhttp化に関して躊躇されてたあたりの理由

  • OkHttpでよくググると出てくるサンプルだと基本例が非同期コールバックベースのコードで起き直しにくい
    • => 日本だとコピペベースで動作検証して導入するケースがやはり多いw
  • AsyncTaskみたいな中に更に非同期コード書くのは微妙*1
    • => 複数通信をまとめるために RxAndroidっぽい処理入れるのは工数かかるよね?

でもココで問題

Android 4.3以下だと、サーバーの設定によりSSL通信で SSLProtocolException 等が出る*2

これ、サーバー側が TLS1.2 のみとか優先?とか端末がサポートしていない設定にされていると、

端末が SSLの通信タイプをサポートしていない場合に発生するようです

  • クライアントで回避方法みたい無いノウハウを探すのが難しい
    • OkHttp3ベースで に書き換えるかー *3

みたいな話になるわけです。


HttpURLConnection => OkHttp化してみる

Okhttpで同期通信がしたい

コールバックを書かなければ同期通信になります

AsyncTask等、既存の処理を極力置き換えず、通信だけであればこちらのほうが都合がいいわけです。*4

  • Getの場合
OkHttpClient client = new OkHttpClient();

Request request = new Request.Builder()
            .header("User-Agent", "UA:hoge") //addHedderで代用可能
            .addHeader("Accept", "hoge") //複数回呼び出し可能  
           .url(url)
           .build();

Response response = client.newCall(request).execute();

if (!response.isSuccessful()){
         //★ response.code() / response.message() では?
     throw new IOException("Unexpected code " + response); 
}
  • postの場合の差分
RequestBody formBody = new FormBody.Builder()
        .add("search", "Jurassic Park")//複数回呼び出し可能
        .build();
Request request = new Request.Builder()
        .url("https://en.wikipedia.org/w/index.php")
        .post(formBody)
        .build();

データ取得自体は

String content = response.body().string();

=> 通常の使い方?レスポンス

InputStream is = response.body().byteStream();

=> JsonPullParser とかで使う

参考

下記は2系の例ですが3系でもそれ程変わってはいないようですね


OkhttpClientの取得

通常の書き方

OkHttpClientは複数回作らないほうがいいようなので

Utilクラス等を作成して作った設定を保持しておきます*5

private static OkHttpClient client;
public static OkHttpClient getOkhttpClient() {
        if(client != null){
        return client;    
    }
    OkHttpClient.Builder builder = new OkHttpClient.Builder()
        .connectTimeout(10, TimeUnit.SECONDS)
        .writeTimeout(10, TimeUnit.SECONDS)
        .readTimeout(30, TimeUnit.SECONDS);

        //キャッシュコントロールとか他の処理も追記できる

        client  = builder.build();
    return client;
}
        long currentTimeMillis = System.currentTimeMillis();
        currentTimeMillis -= 3 * 60 * 60 *1000;//3時間前のデータを消す 

        int MAX_CACHE_SIZE = 10 * 1024 * 1024; // 10 MiB
        File cacheFile = new File(context.getCacheDir(), "responses");
        if(cacheFile.exists() && cacheFile. lastModified() < currentTimeMillis){
              cachedFile.delete();
        }

        Cache cache = new Cache(cacheFile, MAX_CACHE_SIZE);
        builder.setCache(cache);

な感じが良いみたいだけど、でもここら辺やっても、

httpsはキャッシュしないような感じなのであまり意味が無いかもなと*6

SSL(4.3以下でSSLの通信モードをサポートする方法)

public static OkHttpClient getOkhttpSSLClient() {
    ConnectionSpec spec = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
            .tlsVersions(TlsVersion.TLS_1_2, TlsVersion.TLS_1_1, TlsVersion.TLS_1_0, TlsVersion.SSL_3_0)
            .build();
            
    OkHttpClient okHttpClient = new OkHttpClient.Builder() 
    .connectionSpecs(Collections.singletonList(spec)).build();
    return okHttpClient;
}

SSL(テストサーバーに繋ぐ時のオレオレ証明書対応)

@Override
public OkClient createOreOreOkClient() {
    OkHttpClient client = new OkHttpClient();
    final TrustManager[] trustManagers = new TrustManager[]{
            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 null;
                }
            }
    };
    try {
        final SSLContext sslContext = SSLContext.getInstance("SSL");
        sslContext.init(null, trustManagers, new java.security.SecureRandom());
        final SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
        client.setSslSocketFactory(sslSocketFactory);
        client.setHostnameVerifier(new HostnameVerifier() {
            @Override
            public boolean verify(String hostname, SSLSession session) {
                // ホスト名の検証を行わない
                return true;
            }
        });
    } catch (NoSuchAlgorithmException | KeyManagementException e) {
        e.printStackTrace();
    }
    return new OkClient(client);
}

上記の対応でちと問題が。。。

SSL用のOkHttpClientだと、httpのURLにアクセスするときに何故かエラーになってしまいます

通常のOkHttpClientだと

  • http =>通常のモード
  • SSL設定なし=>内部でOSのSSL設定をみて自動設定

と切り替えてくれているようなのですが、ココを自前で遣らないとダメという・・

とくに混在している状況だと結構大変

通信タイプ スキーマ
通常通信(APIとか) https
画像 httpのまま

結構カオスな状況かなと思います*7

これ旨い運用無いのかな〜


その他の周辺的な知識のメモ

Intercepter

以前 id:sakura_bird1 さんが埼玉支部で発表されていやつ

の HttpLoggingInterceptorとか

compile 'com.squareup.okhttp3:logging-interceptor:3.5.0' //★基本okhttpのバージョンと合わせたほうが良いようです
OkHttpClient.builder builder = new OkHttpClient.Builder();
if(BuidConfig.DEBUG){
    HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
    logging.setLevel(Level.BASIC);
        //Cacheレスポンス、オリジナルレスポンス用
    builder.addInterceptor(logging);
        //RedirectやRetryといった中間、ネットワーク発行リクエスト用
        builder.addNetworkInterceptor(logging);
}
OkHttpClient client = builder.build();

Stetho とかは addNetworkInterceptorの方使っているみたいですね。

一応複数 Intercepterは登録できるようで

client.interceptors().add(new DelayInterceptor());
client.networkInterceptors().add(new DelayInterceptor());

な追加イメージになるようです

参考

MockWebServer

Junitのテストとかでよく出てくるやつ

*1:これ非同期処理の中にRealmの非同期処理を書くのも同じく

*2:端末によっては5系端末でもダメ・。。

*3:Picasso/Glide導入する時にセットで入れるし。。

*4:新規であればRxJava/AsyncTaskLoder等で書くのもありですが、既存コードを置き換える場合は面倒

*5:2系から3系になるときにInstanceがライブラリ内で自動保持されるのはやめたみたいなので自前でして欲しいとのことのよう

*6:httpsの仕様なんでしたっけ? でもhttpってどんとん減っていく傾向に有るんですよね?

*7:メディアタイプで分けるとしたとしても、画像URLでhttp/httpsが混在する仕様とかの場合・・