Slim3にTwitter4Jを組み合わせてGAE/JでTwitterを使用する 例。
|
TwitterのOAuth認証を使って、ウェブ上でユーザーが(毎回)ログインする方式の例。
TwitterのOAuth認証を使う為には、事前に自分のアプリケーション専用の認証情報をTwitterから発行してもらう必要がある。
GAEは当然ウェブアプリケーションなので、登録時の「アプリケーションの種類」には「ブラウザアプリケーション」を選択する。
そして、ブラウザアプリの場合は「コールバックURL」を入力する必要がある。
これは、認証が成功した場合に呼ばれるURI。
(クライアントアプリの場合は認証が成功したら暗証番号が表示されるので
それをアプリに入力する必要があるが、
ブラウザアプリの場合は認証が成功したら暗証番号を表示する代わりにコールバックURLにリダイレクトしてくる)
なので、事前にコールバック用URIを決めておく必要がある。
GAEのアプリケーションaaaの場合、「http://aaa.appspot.com/bbb/callback」といったURIがいいんじゃないかと思う。
コーディング(認証部分の処理)の流れは以下の様になる。
| 処理概要 | 例 | |
|---|---|---|
| 1 | 認証に入るURIがリクエストされる。 | http://aaa.appspot.com/bbb/auth |
| 2 | 認証用Controllerが呼ばれる。 Twitter4JのTwitterインスタンスを生成し、HTTPセッションに保存する。 また、認証情報を作成し、Twitterへリダイレクトする。 |
AuthController |
| https://twitter.com/oauth/authenticate?oauth_token=〜 | ||
| 3 | Twitterの認証画面でユーザーがOKする。 すると、コールバックURLにリダイレクトしてくる。 |
|
| http://aaa.appspot.com/bbb/callback?oauth_verifier=〜 | ||
| 4 | コールバック用Controllerが呼ばれる。 HTTPセッションからTwitterインスタンスを取得して認証情報を確認し、OKであれば次画面へ遷移させる。 |
CallbackController |
| * | HTTPセッションからTwitterインスタンスを取得して処理する。 | XxxController |
| 9 | ログアウトしたら、HTTPセッションを削除する。 (Twitterインスタンスを破棄する) |
LogoutController |
Twitterの認証へ行く前のJSPの例。
<a href="http://aaa.appspot.com/bbb/auth">次画面へ</a>
このリンクがクリックされると、サーバー側のAuthControllerが呼ばれる。
「http://aaa.appspot.com/bbb/auth」がリクエストされると、AuthControllerが呼ばれる。
(AuthControllerはgen-controller-without-viewによって生成しておく)
package jp.hishidama.gae.slim3.ex1.server.controller.bbb; import org.slim3.controller.Controller; import org.slim3.controller.Navigation; import twitter4j.*; import twitter4j.http.*;
public class AuthController extends Controller {
private static final String CONSUMER_KEY = "Twitter登録時に表示されたConsumer key";
private static final String CONSUMER_SECRET = "Twitter登録時に表示されたConsumer secret";
@Override
public Navigation run() throws Exception {
if ("127.0.0.1".equals(request.getRemoteHost())) {
return redirect("menu"); //次画面へリダイレクト
}
return redirect(getAuthenticationURL());
}
String getAuthenticationURL() throws TwitterException {
Twitter twitter = new TwitterFactory().getOAuthAuthorizedInstance(CONSUMER_KEY, CONSUMER_SECRET);
sessionScope("twitter", twitter); //セッションに保持
// ■リクエストトークンの作成
RequestToken requestToken = twitter.getOAuthRequestToken(callbackUrl());
sessionScope("requestToken", requestToken); //セッションに保持
return requestToken.getAuthenticationURL();
}
String callbackUrl() {
StringBuffer url = request.getRequestURL(); //「http://aaa.appspot.com/bbb/ccc」
int n = url.indexOf(basePath); //「/bbb/」
n += basePath.length();
url.setLength(n);
url.append("callback");
return url.toString(); //「http://aaa.appspot.com/bbb/callback」
}
}
コールバックURLにはlocalhostを指定できないので、ローカル環境ではOAuth認証を使ったテストが出来ない。
仕方が無いので、IPアドレスが127.0.0.1だった場合は認証をスキップしてそのまま次画面へ遷移しちゃうようにしてみた(苦笑)
Twitterインスタンスは(クライアントアプリと同じく)Consumer
key/secretを指定して生成する。
リクエストトークンの生成は、twitter.getOAuthRequestToken()にコールバックURLを与える。(クライアントアプリの場合は同名メソッドだが引数なしだった)
Twitterインスタンスもリクエストトークンも、HTTPセッションに保存しておく。
リクエストトークンはコールバックされた際に正しい認証だったかどうか確認する為に使用する。
Twitter認証用URLにリダイレクトすることによって、Twitterユーザーのログイン・アプリの許可の確認画面が開く。
一度認証されると、次からはそれらの画面は開かずに直接コールバックURLにリダイレクトされる。
(Twitterからログアウトすると、再度認証が要求される)
Twitter側で認証に成功する(許可される)と、コールバックURLにリダイレクトされてくる。
コールバックURLを「http://aaa.appspot.com/bbb/callback」としておけば、CallbackControllerが呼ばれる。
(CallbackControllerはgen-controller-without-viewによって生成しておく)
package jp.hishidama.gae.slim3.ex1.server.controller.bbb; import org.slim3.controller.Controller; import org.slim3.controller.Navigation; import twitter4j.Twitter; import twitter4j.TwitterException; import twitter4j.http.RequestToken;
public class CallbackController extends Controller {
@Override
public Navigation run() throws Exception {
try {
validateTwitter();
} catch (TwitterException e) {
errors.put("Twitter", e.getMessage());
return forward("error.jsp"); //エラー画面へフォワード
}
return redirect("menu"); //次画面へリダイレクト
}
Twitter validateTwitter() throws TwitterException {
Twitter twitter = sessionScope("twitter"); //セッションから取得
RequestToken requestToken = sessionScope("requestToken"); //セッションから取得
String verifier = asString("oauth_verifier"); //リクエストから取得
twitter.getOAuthAccessToken(requestToken, verifier); //認証の確認
request.getSession().removeAttribute("requestToken"); //セッションから削除
return twitter;
}
}
TwitterインスタンスおよびリクエストトークンをHTTPセッションから取得する。
そしてコールバックURLに付けられていた認証情報(oauth_verifier)を使って、正しく認証されたことを確認する。
認証が成功すればリクエストトークンはもう要らないので、HTTPセッションから削除しておく。
TwitterインスタンスはHTTPセッションの中に残っているので、後続処理内でこれを使ってTwitterにアクセスできる。
次画面へ遷移するのにフォワードでなくリダイレクトを使っている理由は、フォワードだとコールバックURLがブラウザー上にそのまま残ってしまうから。
一応、ログアウト処理として、HTTPセッションを破棄する処理を入れる。
(LogoutControllerはgen-controller-without-viewによって生成しておく)
package jp.hishidama.gae.slim3.ex1.server.controller.bbb; import javax.servlet.http.HttpSession; import org.slim3.controller.Controller; import org.slim3.controller.Navigation;
public class LogoutController extends Controller {
@Override
public Navigation run() throws Exception {
HttpSession session = request.getSession(false);
if (session != null) {
session.invalidate();
}
return redirect(basePath); //indexへ遷移する
}
}
OAuthで毎回認証する方式は毎回ログインしてユーザーが何か作業するタイプなら問題ないが、botの様に不定期に(あるいは定期的に)つぶやきたい場合は、認証された状態を保持しておく必要がある。
ブラウザアプリケーションタイプのOAuth認証は時間が経つと再度認証が必要になるケースもあるだろうから、botには使えない。
そこで、クライアントタイプのアプリケーションのふりをしてOAuthを登録し、アクセストークンを保存することを考えてみた。
クライアントタイプなら暗証番号を一度入力すれば、アクセストークンを保存してユーザーによる認証抜きで何度でもTwitterにアクセスできる。
GAEなら、アクセストークンをDBに保存してしまえばいい。
管理者専用の画面を作って、暗証番号を表示する為のリンクと暗証番号入力エリアを設ければいいだろう。
最初に一度だけ暗証番号を入力する作業を行うというわけ。
まず、暗証番号の入力画面を作成する。
(gen-controllerによってpin.jspを生成しておく)
<%@page pageEncoding="UTF-8" isELIgnored="false" session="false"%>
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions"%>
<%@taglib prefix="f" uri="http://www.slim3.org/functions"%>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>暗証番号設定</title>
<style type="text/css">
.err { color: red; }
</style>
</head>
<body>
<form method="post" action="${ f:url('pinRegist') }">
<h1>暗証番号設定</h1>
<p>Twitterユーザーの暗証番号(アクセストークン)をデータストアに保存する。</p>
<ol>
<li><a href="${ f:url('pinAuth') }" target="_blank">暗証番号を表示する</a></li>
<li>暗証番号の入力:<input type="text" ${ f:text("pin") }/></li>
<li><input type="submit" value="登録"/></li>
</ol>
<%-- メッセージ --%>
<ul>
<c:forEach var="e" items="${f:errors()}">
<li><span class="err">${f:h(e)}</span></li>
</c:forEach>
</ul>
<hr>
<a href="${ f:url('index') }">戻る</a>
</form>
</body>
</html>
使い方としては、
まず、Twitterのログイン画面(暗証番号が表示される)を開くためのリンクをクリックする。(pinAuthが実行される)
そこでログインして暗証番号をメモしたら、次にこの画面に戻ってきて暗証番号を入力する。
最後に登録ボタンを押す(pinRegistが実行される)と完了。
なお、この画面に誰でもアクセスできてしまうと困るので、管理者しかアクセスできないように制限しておくのがいい。
Twitterの認証画面を開く為のControllerを作成する。
(PinAuthControllerはgen-controller-without-viewによって生成しておく)
この認証画面は暗証番号を入力する画面とは別にするのが当然だけど、それはJSP側でaタグのtargetを使って別ウィンドウにしている。
package jp.hishidama.gae.slim3.ex1.server.controller.ccc; import jp.hishidama.gae.slim3.ex1.server.service.ccc.TwitterService; import org.slim3.controller.Controller; import org.slim3.controller.Navigation; import twitter4j.Twitter; import twitter4j.http.RequestToken;
public class PinAuthController extends Controller {
@Override
public Navigation run() throws Exception {
Twitter twitter = new TwitterService().createTwitter();
sessionScope("auth.twitter", twitter);
RequestToken requestToken = twitter.getOAuthRequestToken();
sessionScope("auth.requestToken", requestToken);
String url = requestToken.getAuthorizationURL();
return redirect(url);
}
}
ここでは、Twitterインスタンスからリクエストトークンを作り、それぞれHTTPセッションに格納している。(後で使用する為)
そしてリクエストトークンから認証画面を表示する為のURLを取得し、そこへリダイレクトする。
Twitterインスタンスを生成する為に、TwitterServiceというクラスを作った。Twitter関連の操作をここに集める。
(TwitterServiceはgen-serviceによって生成する)
package jp.hishidama.gae.slim3.ex1.server.service.ccc; import twitter4j.Twitter; import twitter4j.TwitterFactory;
public class TwitterService {
// 読み書きするユーザー用のConsumer key/secret
private static final String CONSUMER_KEY = "Twitter登録時に表示されたConsumer key";
private static final String CONSUMER_SECRET = "Twitter登録時に表示されたConsumer secret";
// 認証されていないTwitterインスタンスを返す
public Twitter createTwitter() {
return new TwitterFactory().getOAuthAuthorizedInstance(CONSUMER_KEY, CONSUMER_SECRET);
}
}
暗証番号を登録する為のControllerを作成する。
(PinRegistControllerはgen-controller-without-viewによって生成しておく)
package jp.hishidama.gae.slim3.ex1.server.controller.ccc; import jp.hishidama.gae.slim3.ex1.server.service.ccc.TwitterService; import org.slim3.controller.Controller; import org.slim3.controller.Navigation; import org.slim3.controller.validator.Validators; import twitter4j.Twitter; import twitter4j.http.AccessToken; import twitter4j.http.RequestToken;
public class PinRegistController extends Controller {
@Override
public Navigation run() throws Exception {
// 項目精査(暗証番号は必須)
if (!validate()) {
return forward("pin.jsp");
}
// セッションから取得
Twitter twitter = sessionScope("auth.twitter");
RequestToken requestToken = sessionScope("auth.requestToken");
if (twitter == null || requestToken == null) {
errors.put("Twitter", "先にTwitterの暗証番号画面を開いてください。");
return forward("pin.jsp");
}
// 暗証番号
String pin = asString("pin");
pin = pin.trim();
try {
AccessToken accessToken = twitter.getOAuthAccessToken(requestToken, pin);
// 対象のユーザーIDであることを確認
String userId = accessToken.getScreenName();
if (!"userId".equals(userId)) {
throw new Exception("ユーザーIDが違います。");
}
// アクセストークンを保存
new TwitterService().storeAccessToken(accessToken);
errors.put("Twitter", "正常に登録できました。"); //面倒なので、正常登録時もエラーメッセージで表示^^;
} catch (Exception e) {
errors.put("Twitter", e.getMessage());
}
return forward("pin.jsp");
}
boolean validate() {
Validators v = new Validators(request);
v.add("pin", v.required());
return v.validate();
}
}
HTTPセッションにTwitterインスタンス等が保存されている前提としている。
保存している箇所は、暗証番号を表示する為のContorllerの中。したがって、もし暗証番号表示を行わずにいきなり暗証番号登録をしようとしたら、HTTPセッションに入っていない状態となる。
暗証番号が正常に通ったら、念の為、本来登録したいユーザーだったのかどうかを確認している。
もし違ってたら、別のユーザーでアクセスすることになっちゃうからねぇ(汗)
ここまで全てOKであれば、アクセストークンをDBに保存する。
保存処理はTwitterServiceに記述している。
import org.slim3.datastore.Datastore; import com.google.appengine.api.datastore.Transaction; import jp.hishidama.gae.slim3.ex1.shared.model.ccc.TwitterToken;
public class TwitterService {
〜
// アクセストークンをDBに保存する
public TwitterToken storeAccessToken(AccessToken accessToken) {
TwitterToken token = new TwitterToken();
token.setAccessToken(accessToken);
Transaction tx = Datastore.beginTransaction();
Datastore.put(token);
tx.commit();
return token;
}
}
DBに保存する為に、TwitterTokenというモデルを作った。
(TwitterTokenはgen-modelで作成し、accessTokenフィールドとセッター・ゲッターメソッドを加えた)
これで、アクセストークンを保存(永続化)することが出来た。
保存されたアクセストークンを使ってTwitterインスタンスを取得するには、以下の様にする。
import jp.hishidama.gae.slim3.ex1.server.meta.ccc.TwitterTokenMeta;
public class TwitterService {
public void tweet(String message) throws TwitterException {
Twitter twitter = loadTwitter();
twitter.updateStatus(message); //ツイートする
}
private Twitter loadedTwitter = null;
Twitter loadTwitter() {
if (loadedTwitter == null) {
AccessToken accessToken = loadAccessToken();
loadedTwitter = new TwitterFactory().getOAuthAuthorizedInstance(CONSUMER_KEY, CONSUMER_SECRET, accessToken);
}
return loadedTwitter;
}
// アクセストークンをDBから取得する
AccessToken loadAccessToken() {
TwitterTokenMeta meta = new TwitterTokenMeta(); //TwitterTokenMetaは、モデルを作った際に一緒に生成される
TwitterToken token = Datastore.query(meta).asSingle();
return token.getAccessToken();
}
}
なお、DBから値を取得するのに使っている「asSingle()」は、データが1つだけ取れる前提で使用するメソッド。
今回の場合は特にチェックしていないので、1個も無ければnullが返るので後続処理でNullPointerExceptionで落ちる。
2個以上ある場合はasSingle()が例外を発生させる。
したがって、暗証番号を2回以上登録する場合は、以前の情報をDBから消しておく必要がある。