2013年6月23日日曜日

Play Framework から Twitter4J を使う。

Twitter 上でのログイン込みで OAuth 認証をを、 Play Framework で Twitter4J を使って試してみる。 せっかく Play を 2.1.1 に上げたところなので、 Play 2.1 の新機能である Filter 機能を使ってみよう。

アプリにアクセスすると Twitter の認証画面が出て、 認証すると Tweet するような感じで作ってみる。


準備


CallbackUrl の設定

Twitter の認証画面から Play の Webアプリに戻ってくるので、 Twitter の 連携アプリケーション管理画面 で アプリケーションに Callback URL を設定する。
今回は ローカルPCで動いているWebアプリなのでCallback先としてローカルホストを指定する。
localhost や localhost.localdomein を指定するとエラーになるので、127.0.0.1 を使う。
ここでは、http://127.0.0.1:9000/authCallbackを指定。

Twitter4J の準備

play new xxx でアプリケーションを作成後、Twitte4J への dependency を追加する。
lib ディレクトリを作成して jar を入れるという方法もあるそうだが、 Build.scala にdependency を追加することにした。
...
  val appDependencies = Seq(
    // Add your project dependencies here,
    jdbc,
    anorm,
    "org.twitter4j" % "twitter4j-core" % "3.0.3"
  )
...

こうしておくと、Play run したときに Maven の セントラルリポジトリ から自動的に持ってくる。
持ってきたライブラリは Play のリポジトリのローカルキャッシュに保存される。
Twitter4J の最新は 3.0.4 になっているが、セントラルリポジトリには配備されていないようなので 3.0.3 を使用。
「"org.twitter4j" % "twitter4j-core" % "3.0.3"」という構文は Scala の構文にはないので、 import している sbt あたりで定義されている演算子 なのだろう。


routes と TwitterHolder


TwittするAction、認証のCallBackを受け取るアクション、ログアウトするActionの3つを用意する。
conf/routes はこんな感じ。
...
GET  /                 controllers.Application.index
GET  /authCallback     controllers.Application.authCallback
GET  /logout           controllers.Application.logout
...

Callback が挟まるため、リクエストを越えて Twitter や RequestToken インスタンスを保持する必要があるので、 それらの保持用のオブジェクトを作っておく。
package twittertest

import twitter4j._
import twitter4j.auth._

object TwitterHolder {

  val CONSUMER_KEY = "xxxxxx";
  val CONSUMER_SECRET = "yyyyyyyyyyyyyy";
  
  private var twitter : Twitter = null;
  
  def getTwitter() : Twitter = 
    if (twitter == null) getNewTwitter() else twitter;
  
  def getNewTwitter() : Twitter = {
    shutdown;
    twitter = (new TwitterFactory()).getInstance();
    twitter.setOAuthConsumer(CONSUMER_KEY, CONSUMER_SECRET);
    return twitter;
  }

  private var requestToken : RequestToken = null;

  def setRequestToken(aRequestToken : twitter4j.auth.RequestToken) { 
     requestToken = aRequestToken;
  }

  def getRequestToken = requestToken;

  private var accessToken : AccessToken = null;

  def setAccessToken(aAccessToken : AccessToken) {
    accessToken = aAccessToken;
  }

  def getAccessToken = accessToken;

  def shutdown {
    if (twitter != null) twitter.shutdown();
    twitter = null;
    requestToken = null;
    accessToken = null;
  }
}

Twitter 側の認証画面でエラーになったりして、RequestToken は作ったが AccessToken がなくなったり、 認証が切れて AccessToken が無効になったりした場合には、 現在の Twitter オブジェクトを破棄して (new TwitterFactory()).getInstance() で新たに Twitter オブジェクトを生成しなおす。


Filter の実装


Filter は play.api.mvc.Filter から派生して作る。
import play.api._
import play.api.mvc._
import twitter4j._
import twittertest.TwitterHolder;

object Global extends WithFilters(
     AuthFilter("authCallback", "logout")) with GlobalSettings;

object AuthFilter {
  def apply(withoutAuthActions : String*) = 
    new AuthFilter(withoutAuthActions);
}

class AuthFilter(withoutAuthActions : Seq[String]) extends Filter {
  import AuthFilter._;

  override def apply(next : (RequestHeader) => Result) 
                    (request : RequestHeader) : Result = {
    val actionInvoked: String 
        = request.tags.getOrElse(play.api.Routes.ROUTE_ACTION_METHOD, "") 
    println("AuthFilter called: " + actionInvoked);

    if (needsAuth(request)) {
      auth(request);
    } else {
      next(request);
    }
  }
  
  private def needsAuth(request : RequestHeader) : Boolean = {
    val actionInvoked: String = request.tags.getOrElse(
        play.api.Routes.ROUTE_ACTION_METHOD, "") 
    if (! withoutAuthActions.contains(actionInvoked)) {
       return TwitterHolder.getAccessToken == null;
    } else {
        return false;
    }
  }
  
  private def auth(request : RequestHeader) : Result = {
    val twitter = TwitterHolder.getNewTwitter();
    val requestToken : twitter4j.auth.RequestToken = twitter.getOAuthRequestToken();
    TwitterHolder.setRequestToken(requestToken);
    controllers.Default.Redirect(requestToken.getAuthorizationURL());
  }
}

作成したフィルタオブジェクトは、Global オブジェクトに登録しておくことで リクエストごとに Play Framework から apply が実行される。 フィルタを複数作ったり、グローバルオブジェクトにほかの設定をしたりすることもあるので、 普通は Global オブジェクトの作成はこんなところについでみたいに書かないでちゃんとしたところに書くのだろうが、 今回はここに書いておいた。

Play Framework の Filter のサンプル では、「認証が必要な Action を指定して」 Filter を作るようになっているが、 全体に認証を書けるようなアプリでは、上のように 認証を実行するAction のように「認証を必要としない Action」を指定するほうがいいと思う。
認証が必要なら、RequestTokenを取得して Twitter の認証画面にリダイレクトする。 リダイレクトは、デフォルトコントローラを使って行う。



Action の実装


Application.scala はこんな感じ。

package controllers

import play.api._
import play.api.mvc._
import twitter4j._;
import twitter4j.auth._;
import twittertest.TwitterHolder;

object Application extends Controller {
  
  def index = Action {
      val twitter = TwitterHolder.getTwitter();
      val message = "Tweet from Play " + scala.compat.Platform.currentTime;
      twitter.updateStatus(message);
      Ok(views.html.index(message));
  }

  def authCallback = Action.apply { request => 
      val twitter = TwitterHolder.getTwitter();

      val authToken : String = request.queryString.get("oauth_token").get.head;
      val authVerifier : String = request.queryString.get("oauth_verifier").get.head;

      val accessToken : AccessToken = twitter.getOAuthAccessToken(TwitterHolder.getRequestToken, authVerifier);
      twitter.verifyCredentials();
      TwitterHolder.setAccessToken(accessToken);
      Redirect("/");
  }  
  
  def logout = Action.apply { request =>
    TwitterHolder.shutdown;
    Redirect("/");
  }    
}

Callback 処理アクション( authCallback ) ではQueryString の oauth_verifier に渡ってくる verifier を使って AccessToken を作成し、verifyCredentials で確認したのちに TwitterHolder に格納して / にリダイレクトする。

index アクションに来たときには もう認証は終わっているはずなので Twitter オブジェクトを取得してupdateStatus する。現在時刻が付加されているのは Twitter の同一内容Tweet拒否機能避け。

再度認証するときには、/logout を実行して認証情報を削除する。

index.scala.html は以下の通り。
@(message: String)

<!DOCTYPE html>

<html><body>
@message<br />
<a href="/"> again </a><br />
<a href="/logout"> logout </a>
</body></html>



今回は認証情報をアプリケーションレベルで保持しているので、すべてのセッションで同じログインになってしまう。
ちゃんと実装するなら、セッションごとにセッションIDを発行して、セッションIDをキーにして Cache に そのセッション用のTwitterHolder のインスタンスを保持するようにしないといけない。

Play Framework 自体は ステートレス施行だが、この手のリクエストをまたがってオブジェクトを使うようなライブラリを使う場合には ステートフルにならざるを得ない。

Play Framework の OAuth サンプル では Sesssion Cookie の Secret 情報も含めて載せてしまっているようだ。このあたりも今度見てみよう。

0 件のコメント:

コメントを投稿