アプリにアクセスすると 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 件のコメント:
コメントを投稿