Android:foursquareでOAuth2認証を行う(その3)

さて、前回までで、foursquareで認証したら、コールバックURLにリダイレクトされて、そのURLをintent-filterで拾うところまで書きました。あともうちょっとでアクセストークンが手に入ります。。。

Login.classのonNewIntentメソッドがコールバックURLをintent-filterでフィルタされることによって呼ばれました。
そのintentには、コールバックURLと、認証コードが入ってます。それを取り出して、アクセストークン取得用のURLにアクセスします。その処理はTokenGetTaskで非同期に行っています。実験した機種はXOOMなのですが、Android 3.0からは通信は非同期に行わないと例外が発生しました。ANR対策は絶対にしろということなんでしょう。

途中、手を抜いているコードもありますがご勘弁。
また、アクセストークンを取得しているOAuthClient.getTokenFromAuthorizationCodeメソッドは、その2に記載しています。(これもまた参照情報のまんまに近いですが)

    /**
     * foursquareからのリダイレクトを受けてコールバックされる処理
     */
    @Override
    protected void onNewIntent(Intent intent) {
        super.onNewIntent(intent);
        Uri uri = intent.getData();
        if (uri == null) {
            return;
        }
        String code = uri.getQueryParameter("code");
        if (code == null) {
            showToast("エラー");
            return;
        }
        // メインスレッドでネットワーク通信したら例外発生したのでAsyncTaskで対応
        new TokenGetTask(this).execute(code);
    }

    private class TokenGetTask extends AsyncTask<String, Void, String> {

        private ProgressDialog mProgressDialog;
        
        public TokenGetTask(Context context) {
            mProgressDialog = new ProgressDialog(context);
        }
        
        @Override
        protected String doInBackground(String... codes) {
            String token = "";
            String code = codes[0];
            try {
                // アクセストークンを取得
                token = OAuthClient.getTokenFromAuthorizationCode(code);
            } catch (ClientProtocolException e) {
                Log.e(TAG, e.getMessage());
            } catch (IOException e) {
                Log.e(TAG, e.getMessage());
            } catch (JSONException e) {
                Log.e(TAG, e.getMessage());
            }
            return token;
        }

        @Override
        protected void onPostExecute(String result) {
            if ("".equals(result)) {
                // エラーでトークンの取得に失敗
                showToast("エラー");
            } else {
                // トークンをPreferencesに保存
                OAuthTokenStore store = new OAuthTokenStore(getApplicationContext());
                store.updateAccessToken(result);
                showToast("アクセストークンを保存しました");
                mProgressDialog.dismiss();
                // アクセストークンを求めた最初のActivityを起動
                startApp();
            }
        }

        @Override
        protected void onPreExecute() {
            super.onPreExecute();
            mProgressDialog.setMessage(getString(R.string.now_loading));
            mProgressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
            mProgressDialog.show();
        }
    }
    
    private void showToast(String text) {
        Toast.makeText(this, text, Toast.LENGTH_LONG).show();
    }

    private void startApp() {
        Intent i = new Intent(this, Main.class);
        startActivity(i);
        finish();
    }

以上で、foursquareからアクセストークンを取得して、Preferencesに保存するまでの処理でした。
これでfoursquareのAPIを呼び出し放題です。(まだあんまりやってないけど・・・)


Android:foursquareでOAuth2認証を行う(その2)

前回では、foursquareにアプリを登録して、コールバックURL、CLIENT_ID、CLIENT_SECRETを取得するところまででした。

次に、実際にOAuth2認証をしてみましょう。
※参照記事ではセキュリティのことを考えて云々とあるのですが、そこらへんは全て飛ばして、とりあえずOAuth2認証が通るところまでを目指します。

ソーシャルサービスのAPIを利用する場合、OAuth等で認証して、アクセストークンをもらいます。アクセストークンを持ってAPIにアクセスすることで、「あぁ、このアクセスは認証済みだな」とサーバにわかってもらう訳です。ひらたくいうと、OAuth認証を行うのは、アクセストークンをもらうことが目的です。

普通のソーシャルサービスでは、アクセストークンに有効期限があって、有効期限が切れたらまた新たなトークンを取得してそれを使う、という流れなのですが、foursquareは、なんと有効期限がありません。一度アクセストークンを取得したらずっとそのままです。とはいっても、foursquareのドキュメントには、有効期限を付けるようにするかもしれないからそのときは対応してね、と書いてあります。

ということで、Androidアプリの処理的には、

  1. アプリにアクセストークンが保存されているか、確認
  2. アクセストークンがなかったら、OAuth認証を行う

という流れになります。

アクセストークンはPreferencesに登録するのがよさげと判断したので、それ用のクラスを作りました。(まぁ参考資料のまんまに近い)

public class OAuthTokenStore {

    private final Context mContext;
    private String mToken;
    
    public OAuthTokenStore(Context context) {
        mContext = context.getApplicationContext();
        mToken = readTokenFromSharedPreferences();
    }
    
    public String getToken() {
        return mToken;
    }
    
    public void updateAccessToken(String accessToken) {
          // エラーの場合を確認をしやすくするために、保存されたトークンを
          // 消せるようにしてある。
          // アプリが完成したらコメントアウト解除
//        if (accessToken == null) {
//            throw new NullPointerException("access token must not be null");
//        }
        mToken = accessToken;
        saveToken();
    }

    private void saveToken() {
        SharedPreferences pref = getPreferences();
        Editor editor = pref.edit();
        editor.putString("accessToken", mToken);
        editor.commit();
    }

    private String readTokenFromSharedPreferences() {
        SharedPreferences pref = getPreferences();
        return pref.getString("accessToken", null);
    }

    private SharedPreferences getPreferences() {
        return PreferenceManager.getDefaultSharedPreferences(mContext);
    }
}

上のクラスを、ActivityのonCreateで呼び出します。

    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // Preferencesからアクセストークンを取得
        OAuthTokenStore store = new OAuthTokenStore(getApplicationContext());
        String accessToken = store.getToken();
        if (accessToken == null) {
            // アクセストークンがない場合の処理
            // foursquareへの認証をさせるためのActivityへ移動
            Intent i = new Intent(this, Login.class);
            startActivity(i);
            finish();
        } else {
            // アクセストークンがある場合の処理
        }

まぁアクセストークンがなかったらいきなりfoursquareの認証画面に移動するような暗黙的Intentを発行してもいいのですが、アプリユーザにとってはいきなりfoursquareに移動して認証しろと言われるのは乱暴なので、一度確認用の画面(Login.class)をはさみました。といっても、次の画面ではfoursquareへ移動のボタンを置いているだけです。ここは簡単なのでレイアウトは割愛。

そのボタンを押したときに実行されるコード。

// Login.class
    /** xmlでonClickを設定 */
    public void login(View v) {
        // ブラウザで4sqへの認証画面を呼び出す
        OAuthClient.login(this);
    }

OAuthCientってなんぞ?となると思いますが、これはOAuthを担当するクラスを作りました。(これも参考資料のまんまに近い)OAuthClient.login(Context);では、foursquareのOAuth2認証用のURLを開く暗黙的Intentを発行しています。

public class OAuthClient {

    private static final String TAG = "OAuthClient";
    
    private static final String ACCESS_TOKEN_URL = "https://ja.foursquare.com/oauth2/access_token";
    
    private static final String AUTHORIZE_URL = "https://ja.foursquare.com/oauth2/authorize";
    
    private static final String CALLBACK_URL = "com.okolabo.android.oauthsample://callback";
    
    private static final String CLIENT_ID = "***************************";
    
    private static final String CLIENT_SECRET = "****************************";
    
    
    /**
     * ブラウザを起動し、認証を行うURLへ遷移する。
     * @param context 呼び出し元のContext
     */
    public static void login(Context context) {
        Uri uri = Uri.parse(AUTHORIZE_URL).buildUpon()
            .appendQueryParameter("client_id", CLIENT_ID)
            .appendQueryParameter("response_type", "code")
            .appendQueryParameter("redirect_uri", CALLBACK_URL)
            .build();
        Intent i = new Intent(Intent.ACTION_VIEW);
        i.setData(uri);
        context.startActivity(i);
    }
    
    /**
     * Authorization Code からアクセストークンを取得します。
     * エラーだった場合は{@code null}を返します。
     * 
     * @param code Authorization Code
     * @return 取得したアクセストークンを返します。
     * @throws ClientProtocolException
     * @throws IOException
     * @throws JSONException
     */
    public static String getTokenFromAuthorizationCode(String code) throws ClientProtocolException, IOException, JSONException {
        String token = null;
        DefaultHttpClient client = new DefaultHttpClient();
        Uri uri = Uri.parse(ACCESS_TOKEN_URL).buildUpon()
            .appendQueryParameter("client_id", CLIENT_ID)
            .appendQueryParameter("client_secret", CLIENT_SECRET)
            .appendQueryParameter("grant_type", "authorization_code")
            .appendQueryParameter("redirect_uri", CALLBACK_URL)
            .appendQueryParameter("code", code)
            .build();
        HttpGet httpGet = new HttpGet(uri.toString());
        HttpResponse response = client.execute(httpGet);
        HttpEntity entity = response.getEntity();
        String res = EntityUtils.toString(entity);
        entity.consumeContent();
        JSONObject json = new JSONObject(res);
        if (json.has("access_token")) {
            token = json.getString("access_token");
        } else {
            if (json.has("error")) {
                String error = json.getString("error");
                Log.d(TAG, error);
            }
        }
        client.getConnectionManager().shutdown();
        return token;
    }
}

認証用のページに行くと、「登録したアプリから認証が求められているが、大丈夫ならログインしてください」というメッセージが出ます。まぁここはログインしてください。ログインすると、認証コード付きでコールバックURLにリダイレクトされます。

リダイレクトURLを拾わないと認証が進まないので、リダイレクトを拾います。
AndroidManifest.xmlのintent-filterを設定します。
あと、uses-permissionにandroid.permission.INTERNETを追加しといて下さい。

        <activity
            android:name="Login"
            android:launchMode="singleTask"
            >
            <intent-filter>
                <action android:name="android.intent.action.VIEW" />
                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />
                <data android:scheme="com.okolabo.android.oauthsample" android:host="callback" />
            </intent-filter>
        </activity>

これを設定しておいたら、ログインボタンを置いていたLogin.classがintent-filterによって呼び出されます。launchMode=”singleTask”にしておきます。これにより、すでにLogin.classは起動しているため、onNewIntentメソッドが呼ばれます。コールバックURLで呼ばれるコールバックが、Login.classのonNewIntentメソッドだったと思って下さい。

次の記事へ移動


Android:foursquareでOAuth2認証を行う(その1)

foursquareのWebAPIを使ったアプリを作ろうとしているので、そのメモを残します。
foursquareはOAuth2認証で認証を行っています。OAuth認証よりもさらに簡易になったらしいのですが、OAuth認証をやったことがない自分はよくわかってません。とりあえず、foursquareの資料とmixiアプリでのOAuth2の資料を見ながらやってみました。

参考資料は以下。

まずは、foursquareにアプリケーションの登録を行います。
これをすることで、認証に必要な情報を取得しておきます。

こちらから。

そこで、適当にアプリケーション名などを決めておきます。
コールバックURLは、ユニークになる必要があるので、Androidアプリのパッケージ名をスキーマにしてしまいます。これが後に重要になります。認証後にリダイレクトされたときにAndroidでintent-filterを使って自分のfoursquareアプリを起動させることになるからです。

登録が終わると、以下のようか感じで、CLIENT_IDとCLIENT_SECRETが登録されます。

OAuth2認証に必要なのは、以下の3つです。

  • コールバックURL
  • CLIENT_ID
  • CLIENT_SECRET

これで、必要な情報は揃いました。

次の記事へ


git:特定のタグのソースをエクスポートする。

gitで指定したタグのプロジェクトをエクスポートしたかったのだけれども、やり方がわからなかった。色々やってみたところ、これで上手くいった。v1.0.0がタグ名。prefixは最後にスラッシュがないとprefixがついてしまうだけになるらしいので、必須。部タグの前にブランチ名を入れるような例もあったのだが、masterっていれてもうまくいかなかったが、なくしたらなぜか成功した。

最新のものが欲しい場合は、タグのところをHEADにすればよい。

git archive --format zip --prefix=export/ v1.0.0 > export.zip

Slim3:セッションを使う

今日、オコラボのサイトを暫定で完成としてデプロイしましたー!パチパチパチ!!
暫定で完成ってのも変な表現だけど。

http://www.okolabo.com

自社サイトをSlim3使ってGAE上に構築するというのは、ローコストかつ安定してるので、機会損失が自社サーバで行うよりも少ないだろうと思います。こういう提案もしていきたいなぁ〜と思ってます。アクセス数によるけど、基本的に維持費はドメイン代だけで済むし。有料になるくらいアクセス集中するって、よっぽどですからね。

さて、話はSlim3でセッションを使うです。PHPerな自分はそういえばSlim3でセッションを使うようなことをしたことがなかった!セッションを使いたかった理由は、お問い合わせページでリロードによる連投を防ぐためです。

まず、appengine-web.xmlのsessions-enabledをtrueにします。

<sessions-enabled>true</sessions-enabled>

これでセッションが使えるようになりました。

セッションの設定方法、取得方法、削除方法は以下の通りです。

// セッション "key"に値"value"を設定する。
sessionScope("key", "value");

// セッション "key"を取得する。
String key = sessionScope("key");

// セッション "key"を削除する。
removeSessionScope("key");

セッションのデータはDatastoreにどんどん蓄積されていくので、定期的に削除してやらなければならない模様。どうも既にセッションデータをクリーンするサーブレットは準備されているらしいので、cronで定期的に削除してやればよいらしいっす。
web.xmlに以下を追加します。

<servlet>
	<servlet-name>_ah_sessioncleanup</servlet-name>
	<servlet-class>com.google.apphosting.utils.servlet.SessionCleanupServlet</servlet-class>
</servlet>
<servlet-mapping>
	<servlet-name>_ah_sessioncleanup</servlet-name>
	<url-pattern>/_ah/sessioncleanup</url-pattern>
</servlet-mapping>
<security-constraint>
	<web-resource-collection>
		<web-resource-name>session-cleanup</web-resource-name>
		<url-pattern>/_ah/sessioncleanup</url-pattern>
	</web-resource-collection>
	<auth-constraint>
		<role-name>admin</role-name>
	</auth-constraint>
</security-constraint>

また、cron.xmlに以下のようにします。(cron.xmlがない場合はWEB-INF以下に作成)

<?xml version="1.0" encoding="UTF-8"?>
<cronentries>
	<cron>
		<url>/_ah/sessioncleanup?clear</url>
		<description>Clean up sessions</description>
		<schedule>every 12 hours</schedule>
	</cron>
</cronentries>

以上でーす。