2016年末のAndroidでのSSL対応に関して
- はじめに
- 近々の状況
- 去年の Android 6.0対応あたりで
- でもココで問題
- HttpURLConnection => OkHttp化してみる
- OkhttpClientの取得
- その他の周辺的な知識のメモ
はじめに
この記事は Android その3 Advent Calendar 2016 - Qiita
の六日目の記事です。
- 昨日は matsuokah - Qiita さんのAndroidのコードを書く前にコーディングに集中できる状態を作る - will and wayのお話です
- 明日は kimukou - QiitaさんのGenymotion 2.8.1とGoogle Play Services - exception thinkのお話です
近々の状況
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の通信タイプをサポートしていない場合に発生するようです
みたいな話になるわけです。
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だと
と切り替えてくれているようなのですが、ココを自前で遣らないとダメという・・
とくに混在している状況だと結構大変
通信タイプ | スキーマ |
---|---|
通常通信(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());
な追加イメージになるようです
参考
Interceptor/networkInterceptor の違い
自作Intercepterの話
MockWebServer
Junitのテストとかでよく出てくるやつ