2012年4月30日月曜日

AndroidでGoogleのOAuth2認証を行うには?(外部ライブラリ完全非依存版)

AndroidでGoogleのOAuth2認証を行う必要が出てきた。

今時のアプリではOAuth2が必要な場面は相当多いものの、いかんせん複雑で、認証してやりたいことができるようになるまでのプロセスが非常に長い。

Andorid上では初めて行ったので、できるだけ短く、分かりやすくまとめておこうと思う。

追記12.08.21:以下はWebViewを使用する方法だけど、AccountManagerを使用する方法もある。
琴線探査: AndroidのAccountManager経由でGoogleのOAuth2認証を行うには?(外部ライブラリ完全非依存版)
----

OAuth2認証の流れ

複雑で長いプロセスの場合は、まず大きな流れを掴んでおくことが重要だ。GoogleのOAuth2の流れはこんな感じ。

  1. Googleの「API Console」に自分のアプリを登録
  2. 「API Console」でOAuth2認証で必要な情報を集める(Client ID、Client Secret, Redirect URIs)
  3. 「OAuth 2.0 Playground」を使って許可範囲(scope)を決定する
  4. 集めた情報を使ってWebViewでOAuth2認証ページを表示する
  5. OAuth2認証ページでユーザー自身でアプリケーションからの接続の許可をしてもらう
  6. ユーザー許可後のページタイトルから「code」を取得する
  7. 「code」を使って「アクセストークン」を取得する

このように、アクセストークンを取得することが最終目的となる。

一度アクセストークンを取得出来れば、あとは各種APIにリクエストする時にこのアクセストークンを使えばいい。


Googleのライブラリを使うべきか?

OAuth2の基本的な流れはここに書いてある。

Using OAuth 2.0 to Access Google APIs - Google Accounts Authentication and Authorization — Google Developers

その中で「Google APIs Client Library for Java」という専用のライブラリが紹介されている。

google-api-java-client - Google APIs Client Library for Java - Google Project Hosting

恐らく、これを使うのがスジなのだろうし、使い慣れれば色々と便利なこともあるのだろう。しかし、今回は一切ライブラリを使わないことにした。なぜなら

  • dependenciesがありすぎるので重いし、
  • Android用にdependenciesをクリアしなければならないので面倒だし、
  • Androidでのサンプルが見当たらなかった

から。

必要な処理を書く前に、まずライブラリの使い方から学ばなければならないという、よくあるケースだ。こういうのは正直だるいし、うんざりしている。

直々にHTTPリクエストして直々にjsonレスポンスを処理した方がよほど直感的だと思うので、外部ライブラリに一切依存せずに書くことにした。


「API Console」にてOAuth2認証で必要な情報を集める

コーディングを始める前に、まずOAuth2で必要な情報を集めておく必要がある。

まず「API Console」に逝って、プロジェクトを作成する。ここではプロジェクト名を「OAuth2Google」とした。

プロジェクトを作ると自動的に開く「services」で必要なAPIを「ON」する。今回はユーザーのプロフィール情報を取得するだけだったので何もONしなかった。

「API Access」ページに逝って「Create an OAuth 2.0 client ID」という青くてデカいボタンクリック。

「Product Name」を入力して「Next」。ここでは「OAuth2Google」とした。「Product Logo」は放っておいてよし。

「Application Type」として「Installed Application」を選んで「Create client ID」。

すると、「Client ID」「Client secret」「Redirect URIs」がもらえる。これらがOAuth2で必要な情報だ。



許可範囲(scope)を決定する

OAuth2では、ユーザーからどのAPIに対する許可を得るか(scope)を決めておかなければならない。

OAuth 2.0 Playground」で各APIのURIのリストを確認する。

今回は「Userinfo - Profile https://www.googleapis.com/auth/userinfo.profile」をscopeとして使う。

必要なAPIが複数ある場合は、このようにURIをスペースで区切って記述する。

String scope = "https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email";


初期画面を作る


res/layout/main.xmlで、このような単純な初期画面(StartOAuth2GoogleActivity.java)をつくる。


初期画面から認証画面へIntentを投げる

上の初期画面でボタンをクリックするとOAuth2認証画面を表示する。

OAuth2認証画面は汎用性を考えて、OAuth2GoogleActivity.javaとして別アクティビティで定義した。

ボタンクリック時に呼ばれるonClickBtn()では、認証に必要な情報を認証画面に対してIntentで投げている。

// ボタンをクリックした時に呼ばれる
  // OAuth2GoogleActivityに逝く
  public void onClickBtn(View view) {
    Log.v("onClickBtn", "OAuth2開始!");
    Intent intent = new Intent(this, OAuth2GoogleActivity.class);
    intent.putExtra(OAuth2GoogleActivity.CLIENT_ID, CLIENT_ID);
    intent.putExtra(OAuth2GoogleActivity.CLIENT_SECRET, CLIENT_SECRET);
    intent.putExtra(OAuth2GoogleActivity.SCOPE, SCOPE);
    startActivityForResult(intent, OAuth2GoogleActivity.REQCODE_OAUTH);
  }

認証画面をstartActivityForResult()で呼び出して、後で初期画面に戻ってきた時に初期画面に定義してあるonActivityResult()を呼び出してもらうようにする。


認証画面を作る

res/layout/oauth2google.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >

    <ViewSwitcher
        android:id="@+id/vs"
        android:layout_width="match_parent"
        android:layout_height="match_parent" >

        <FrameLayout
            android:id="@+id/viewProg"
            android:layout_width="match_parent"
            android:layout_height="match_parent" >

            <LinearLayout
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center"
                android:gravity="center"
                android:orientation="vertical" >

                <ProgressBar
                    android:id="@+id/progBar"
                    style="?android:attr/progressBarStyleLarge"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:padding="5dp" />

                <TextView
                    android:id="@+id/tvProg"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="OAuth2認証ページに接続中..."
                    android:textAppearance="?android:attr/textAppearanceMedium" />
            </LinearLayout>
        </FrameLayout>

        <WebView
            android:id="@+id/wv"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
    </ViewSwitcher>

</LinearLayout>

OAuth2認証画面は、ViewSwitcherを使ってプログレスバーとWebVIewを交互に表示する。


プログレスバーを表示しつつIntentから認証に必要な情報を得る

認証画面起動時には、まずプログレスバーを表示する。


この時、Intentから認証に必要な情報を取得する。

// Intentからパラメータ取得
    Intent intent = getIntent();
    clientId = intent.getStringExtra(CLIENT_ID);
    clientSecret = intent.getStringExtra(CLIENT_SECRET);
    scope = intent.getStringExtra(SCOPE);


認証ページを表示する

// WebView設定
    wv.setWebViewClient(new WebViewClient() { // これをしないとアドレスバーなどが出る


      @Override
      public void onPageFinished(WebView view, String url) { // ページ読み込み完了時

        // ページタイトルからコードを取得
        String title = view.getTitle();
        String code = getCode(title);

        // コード取得成功ページ以外
        if (code == null) {
          Log.v("onPageFinished", "コード取得成功ページ以外 url=" + url);
          if (!(vs.getCurrentView() instanceof WebView)) { // WebViewが表示されてなかったら
            vs.showNext(); // Web認証画面表示
          }
        }

        // コード取得成功
        else {
          Log.v("onPageFinished", "コード取得成功 code=" + code);
          vs.showPrevious(); // プログレス画面に戻る
          new TaskGetAccessToken().execute(code); // アクセストークン取得開始
        }

      }
    });

    // 認証ページURL
    String url = "https://accounts.google.com/o/oauth2/auth" // ここに投げることになってる
        + "?client_id=" + clientId // アプリケーション登録してもらった
        + "&response_type=code" // InstalledAppだとこの値で固定
        + "&redirect_uri=urn:ietf:wg:oauth:2.0:oob" // タイトルにcodeを表示する場合は固定
        + "&scope=" + URLEncoder.encode(scope); // 許可を得たいサービス

    Log.v("onCreate", "clientId=" + clientId + " clientSecret=" + clientSecret + " scope=" + scope + " url=" + url);

    // 認証ページロード開始
    wv.loadUrl(url);

インテントから取得した認証に必要な情報を使ってURLを構築し、認証ページのロードを開始する。

WebViewに対してセットしてあるWebViewClientのonPageFinished()で、初期ページのロードが完了したらvs.showNext()でプログレスバー画面からWebViewの認証画面に切り替える。

IDとPWを入力していないなら入力を求められる。


すでに入力済みならイキナリこのような画面になる。



コード取得成功ページのタイトルから「code」を抽出する

ユーザーが「アクセスを許可」するとコード取得成功ページに切り替わる。

このページのタイトルは「Success code=XXXXXXX」という風になっていて、ここから「code」の部分をこのように抽出する。

/**
   * 認証成功ページのタイトルは「Success code=XXXXXXX」という風になっているので、
   * このタイトルから「code=」以下の部分を切り出してOAuth2アクセスコードとして返す
   * 
   * @param title
   *          ページタイトル
   * @return OAuth2アクセスコード
   */
  protected String getCode(String title) {
    String code = null;
    String codeKey = "code=";
    int idx = title.indexOf(codeKey);
    if (idx != -1) { // 認証成功ページだった
      code = title.substring(idx + codeKey.length()); // 「code」を切り出し
    }
    return code;
  }

ここで、WebViewに対してセットしておいたWebViewClientのonPageFinished()のコード取得成功ケースの「else」に入る。

そこでvs.showPrevious()で再びプログレスバー表示に切り替えて、TaskGetAccessTokenというAsyncTaskを起動する。


アクセストークンを取得する


内部クラスTaskGetAccessToken

// アクセストークン取得タスク
  protected class TaskGetAccessToken extends AsyncTask<String, Void, String> {
    @Override
    protected void onPreExecute() {
      Log.v("onPostExecute", "アクセストークン取得開始");
      tvProg.setText("アクセストークンを取得中...");
    }


    @Override
    protected String doInBackground(String... codes) {
      String token = null;
      DefaultHttpClient client = new DefaultHttpClient();
      try {

        // パラメータ構築
        ArrayList<NameValuePair> formParams = new ArrayList<NameValuePair>();
        formParams.add(new BasicNameValuePair("code", codes[0]));
        formParams.add(new BasicNameValuePair("client_id", clientId));
        formParams.add(new BasicNameValuePair("client_secret", clientSecret));
        formParams.add(new BasicNameValuePair("redirect_uri", "urn:ietf:wg:oauth:2.0:oob"));
        formParams.add(new BasicNameValuePair("grant_type", "authorization_code"));

        // トークンの取得はPOSTで行うことになっている
        HttpPost httpPost = new HttpPost("https://accounts.google.com/o/oauth2/token");
        httpPost.setEntity(new UrlEncodedFormEntity(formParams, "UTF-8")); // パラメータセット
        HttpResponse res = client.execute(httpPost);
        HttpEntity entity = res.getEntity();
        String result = EntityUtils.toString(entity);

        // JSONObject取得
        JSONObject json = new JSONObject(result);
        if (json.has("access_token")) {
          token = json.getString("access_token");
        } else {
          if (json.has("error")) {
            String error = json.getString("error");
            Log.d("getAccessToken", error);
          }
        }

      } catch (ClientProtocolException e) {
        e.printStackTrace();
      } catch (IOException e) {
        e.printStackTrace();
      } catch (JSONException e) {
        e.printStackTrace();
      } finally {
        client.getConnectionManager().shutdown();
      }
      return token;
    }


    @Override
    protected void onPostExecute(String token) {
      if (token == null) {
        Log.v("onPostExecute", "アクセストークン取得失敗");
      } else {
        Log.v("onPostExecute", "アクセストークン取得成功 token=" + token);
        Intent intent = new Intent();
        intent.putExtra(ACCESS_TOKEN, token);
        setResult(Activity.RESULT_OK, intent);
      }
      finish();
    }

  } // END class TaskGetAccessToken

このタスクが起動されると、まずonPreExecute()が呼ばれてプログレスバーの文字表示を「アクセストークンを取得中...」と変更する。

次にdoInBackground()が呼び出される。ここがメイン。トークンの取得はPOSTで送ることになっているので注意が必要。レスポンスはJSONで返ってくるのでJSONObjectに変換してアクセストークンを取得する。

最後にonPostExecute()が呼ばれる。成功なら取得したトークンをIntentに含めて初期画面へ投げる。成功の場合も失敗の場合もとにかくfinish()で初期画面へ戻る。

OAuth2の認証プロセスとしては、ここまでで終了ということになる。


アクセストークンを使ってユーザー情報の取得開始

認証画面から初期画面へ戻ってきたらonActivityResult()が呼ばれるようにしてあるのでこのコードが呼ばれる。

// OAuth2GoogleActivityから戻ってきた時に呼ばれる
  // 認証ができていたらユーザー情報を取得する
  @Override
  protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    Log.v("onActivityResult", "OAuth2プロセスから帰還!");
    super.onActivityResult(requestCode, resultCode, data);
    boolean validData = false;
    if (data != null) {
      String token = data.getStringExtra(OAuth2GoogleActivity.ACCESS_TOKEN);
      if (token != null) {
        new TaskGetUserInfo().execute(token);
        validData = true;
      }
    }
    if (!validData) {
      Log.v("onActivityResult", "アクセストークンなし");
      tvName.setText("OAuth2プロセスが正常に行われませんでした");
    }
  }

Intentからアクセストークンを取得する。実際のアプリで使う場合はここでプリファレンスなどに保存することになるだろう。

トークンが取得できたら、ユーザーの名前を表示するためにTaskGetUserInfoというユーザー情報を取得するAsyncTaskを起動する。


ユーザー情報からユーザーの名前を表示する

内部クラスTaskGetUserInfo

// ユーザー情報取得タスク
  protected class TaskGetUserInfo extends AsyncTask<String, Void, JSONObject> {


    @Override
    protected void onPreExecute() {
      Log.v("onPreExecute", "ユーザー情報取得開始");
      tvName.setText("ユーザー情報を取得しています...");
    }


    @Override
    protected JSONObject doInBackground(String... accessTokens) {
      JSONObject userInfo = null;
      String url = "https://www.googleapis.com/oauth2/v2/userinfo" // ここに投げることになってる
          + "?access_token=" + accessTokens[0]; // OAuth2プロセスでもらった
      DefaultHttpClient client = new DefaultHttpClient();
      try {
        HttpGet httpPost = new HttpGet(url);
        HttpResponse res = client.execute(httpPost);
        HttpEntity entity = res.getEntity();
        String result = EntityUtils.toString(entity);
        Log.v("doInBackground", "result=" + result);
        userInfo = new JSONObject(result);
      } catch (ClientProtocolException e) {
        e.printStackTrace();
      } catch (IOException e) {
        e.printStackTrace();
      } catch (JSONException e) {
        e.printStackTrace();
      } finally {
        client.getConnectionManager().shutdown();
      }
      return userInfo;
    }


    @Override
    protected void onPostExecute(JSONObject userInfo) {
      if (userInfo == null) {
        tvName.setText("ユーザー情報取得失敗(userInfoがNULL)");
        Log.v("onPostExecute", "ユーザー情報取得失敗 userInfoがNULL");
      } else {
        String name = null;
        try {
          name = userInfo.getString("name");
          tvName.setText("あなたの名前は [" + name + "] ですね?");
          Log.v("onPostExecute", "ユーザー情報取得成功 name=" + name + " userInfo=" + userInfo.toString());
        } catch (JSONException e) {
          tvName.setText("ユーザー情報取得失敗");
          Log.v("onPostExecute", "ユーザー情報取得失敗 userInfo=" + userInfo.toString());
          e.printStackTrace();
        }
      }
    }


  } // END class TaskGetUserInfo


まずonPreExecute()が呼ばれてこのような画面になる。


次にdoInBackground()が呼ばれる。ここでユーザー情報を得るためのAPIにアクセストークンを使ってアクセスしている。

最後にonPostExecute()が呼ばれ、このようにユーザーの名前を表示する。



まとめ

最後にビデオで遷移をまとめ。



あまりのあっけなさに虚しさを禁じ得ない。たったこれだけのために、どんだけ苦労が必要なんだぁ〜!・・・と_| ̄|○

もし似たような事をしようとしている方がいらしたら、是非参考にして頂ければと思います。

Eclipseプロジェクトファイル:OAuth2Google.zip(189KB)

今回は大いにこちらを参考にさせていただきました。

[Android] Android+Twitter4JでOAuthするためのソースコード - adakoda

Twitter4Jを使う場合なら、参考というか、ほぼそのまま使えてしまうのでは?と思う、すばらしいデザインのコードです。

いや、できるだけ短く書くつもりだったのに、結局長かった。