Volley + oauth-signpost로 POST 요청

과거에 자신의 블로그에 쓴 것을 Qiita에도 전재.

완성판의 플래그의 취급이 우선 그랬기 때문에 수정.

자작 Tumblr 클라이언트 앱의 통신 주위는, oauth-signpost를 사용한 자전 구현으로 하고 있었습니다.

그러나 에러 핸들링의 용이성과 단순하게 사용하고 싶다고 하는 이유로 구현을 Volley로 재작성하기 위한 조사를 실시했습니다.

역시 수요는 있는 것 같고, HurlStack를 확장해 OAuth의 서명을 하는 선인의 지혜를 몇개인가 찾아낼 수 있었습니다.

  • Android - Volley + oauth-signpost에서 OAuth 요청 - Qiita
  • volley + signpost

  • 그러나 이러한 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 요청이 인수로 전달됩니다.

    그래서이 두 가지 방법을 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 요청을 서명할 때는 서명하기 전에 다음 두 점이 필요합니다.
  • POST 매개 변수 + 끝점 URL을 매개 변수로 Consumer로 설정
  • HttpURLConnection setRequestMethod("POST")setDoOutput(true) 설정

  • 이것, 한 번 스스로 oauth-signpost 사용한 실장하고 있었기 때문에 알았지만, 하지 않았다면 도중에 좌절했을 것이다…

    이번에 만든 것은 Gists에도 올리고 있습니다.
    OAuth signed POST request with Volley + oauth-signpost

    자작의 Tumblr 클라이언트도 Google Play에서 공개하고 있으므로 잘 부탁드립니다.
    Android app on Google Play

    좋은 웹페이지 즐겨찾기