過去に自分のブログに書いたものをQiitaにも転載。
完成版のフラグの扱いがまずそうだったので修正。
自作Tumblrクライアントアプリの通信周りは、oauth-signpostを使った自前実装で行っていました。
しかし、エラーハンドリングの容易さと、単純に使ってみたいと思ったという理由から実装をVolleyに書き換えるための調査を行いました。
やはり需要はあるようで、HurlStackを拡張してOAuthの署名を行う先人の知恵を幾つか見つけることができました。
しかし、これらのHurlStack拡張はGETリクエストに対しては有効に動作しますが、この方法でPOSTリクエストを行うと401が帰ってきてしまいます。
E/Volley﹕ [46076] BasicNetwork.performRequest: Unexpected response code 401 for http://api.tumblr.com/v2/blog/hogehoge.tumblr.com/post/reblog
問題
POSTリクエストの場合、署名する前にconsumerにPOSTパラメータ等を渡してあげる必要がありますが、前述の例では開いたコネクションにすぐ署名しているためパラメータが足りていない状態です。
そこで、さらに前述のHurlStack拡張を修正していこうということになるのですが、これが意外に苦労しました。
まず、POSTリクエストを署名するにあたって、リクエストのパラメータが必要になるのですが、前述のHurlStack拡張が署名を行っているcreateConnection
メソッドではVolleyのリクエストを参照できません。
では、他のメソッドはどうかと確認すると、そもそもOverride可能なのはcreateConnection
のほかにはperformRequest
だけです。
そして、performRequest
にはVolleyリクエストが引数で渡されています。
なので、この2つのメソッドをOverrideしてPOSTリクエストを署名する方法を考えていきます。
試行錯誤
HurlStack.java - platform/frameworks/volley - Git at Google
HurlStack.javaを読んでみると、performRequest
の中からcreateConnection
を呼ぶことでコネクションを生成しているようです。
なので、Volleyリクエストが引数として渡されているperformRequest
内でOAuth用のパラメータをconsumerに設定する方法を試してみます。
なお、protectedなRequest#getParams()
にアクセスするため、自パッケージ内のRequest拡張にキャストしています。
public class OAuthPostHurlStack extends HurlStack {
private final OAuthConsumer mConsumer;
public OAuthPostHurlStack(OAuthConsumer consumer) {
mConsumer = consumer;
}
@Override
public HttpResponse performRequest(Request<?> request, Map<String, String> additionalHeaders)
throws IOException, AuthFailureError {
if (request.getMethod() == Request.Method.POST) {
if (request instanceof CustomRequest) {
// POSTパラメータをHttpParametersに詰め替える
HttpParameters oauthParams = new HttpParameters();
Map<String, String> params = ((CustomRequest) request).getParams();
for (String key : params.keySet()) {
oauthParams.put(key, OAuth.percentEncode(params.get(key)));
}
// POSTパラメータに加えてエンドポイントのURLも必要
oauthParams.put("realm", request.getUrl());
// consumerのパラメータとして設定
mConsumer.setAdditionalParameters(oauthParams);
}
}
return super.performRequest(request, additionalHeaders);
}
@Override
protected HttpURLConnection createConnection(URL url) throws IOException {
// 最初に紹介した先人のものと一緒なので省略
return connection;
}
}
これでうまくいくのでは?と思い実行してみるもまたも401エラー…なにかが足りない…?
E/Volley﹕ [46076] BasicNetwork.performRequest: Unexpected response code 401 for http://api.tumblr.com/v2/blog/hogehoge.tumblr.com/post/reblog
解決
改めて、自前の実装とHurlStack.javaを見比べて不明な設定が無いか確認してみると、自前の実装では署名の前に、HttpURLConnectionのsetRequestMethod("POST")
とsetDoOutput(true)
を呼んでいました。
元のHurlStack.javaでは、当然ですがこの処理はcreateConnection
より後ろで行われています。
この際、HurlStackを拡張するのではなく、元から書き換えてしまおうかと一瞬頭をよぎりましたが、この2つの処理は本来の処理の前に1回実行してしまっても問題ないだろうと判断し署名前に設定することにしました。
public class OAuthPostHurlStack extends HurlStack {
private final OAuthConsumer mConsumer;
private ArrayList<String> mOauthSignedPosts = new ArrayList<>();
public OAuthPostHurlStack(OAuthConsumer consumer) {
mConsumer = consumer;
}
@Override
public HttpResponse performRequest(Request<?> request, Map<String, String> additionalHeaders)
throws IOException, AuthFailureError {
if (request.getMethod() == Request.Method.POST) {
if (request instanceof CustomRequest) {
// OAuthで署名すべきPOSTリクエストのURLを格納する
mOauthSignedPosts.add(request.getUrl());
// POSTパラメータをHttpParametersに詰め替える
HttpParameters oauthParams = new HttpParameters();
Map<String, String> params = ((CustomRequest) request).getParams();
for (String key : params.keySet()) {
oauthParams.put(key, OAuth.percentEncode(params.get(key)));
}
// POSTパラメータに加えてエンドポイントのURLも必要
oauthParams.put("realm", request.getUrl());
// consumerのパラメータとして設定
mConsumer.setAdditionalParameters(oauthParams);
}
}
return super.performRequest(request, additionalHeaders);
}
@Override
protected HttpURLConnection createConnection(URL url) throws IOException {
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
try {
if (mOauthSignedPosts.contains(url)) {
// POSTリクエストのコネクションは署名前に設定が必要
connection.setRequestMethod("POST");
connection.setDoOutput(true);
mOauthSignedPosts.remove(url)
}
mConsumer.sign(connection);
} catch (OAuthMessageSignerException e) {
e.printStackTrace();
} catch (OAuthExpectationFailedException e) {
e.printStackTrace();
} catch (OAuthCommunicationException e) {
e.printStackTrace();
}
return connection;
}
}
これで無事、POSTリクエストも署名することができました。
まとめ
oauth-signpostでHttpURLConnectionのPOSTリクエストを署名するときは、署名の前に下記の2点が必要
- POSTパラメータ+エンドポイントのURLをパラメータとしてconsumerに設定する
- HttpURLConnectionの
setRequestMethod("POST")
とsetDoOutput(true)
を設定する
これ、一回自分でoauth-signpost使った実装してたから分かったけど、やってなかったら途中で挫折しただろうな…
今回作ったものはGistsにも上げています。 [OAuth signed POST request with Volley + oauth-signpost](https://gist.github.com/chibatching/a59e30def6956da04b2f)