読者です 読者をやめる 読者になる 読者になる

JSF2+CDI+Twitter4Jでカンバセーション内でOAuth認証を行う

Java

個人的にハマりどころがあったのでJSF2+CDI+Twitter4JでのOAuth認証の方法をポイントだけまとめます。
JBoss7.1.0と7.1.1、GlassFish3.1.2で動作の確認をしています。

Twitterインスタンスを提供します。
TwitterFactoryはスレッドセーフのようなので共有し、Twitterをセッションに供給します。

@ApplicationScoped
public class TwitterProducer {
    
    private TwitterFactory twitterFactory = new TwitterFactory();
    
    @SessionScoped
    @Produces
    protected Twitter getTwitter() {
        return twitterFactory.getInstance();
    }
}

供給されたTwitterを利用してTwitterの認証系処理を行います。
状態を持つ必要はないのでステートレスにしています。

@Stateless
public class TwitterOAuthService {

    @Inject
    private Twitter twitter;

    /**
     * Twitter認証用URLを取得する
     */
    public RequestToken getRequestToken(String callbackURL) throws TwitterException {
        RequestToken requestToken = twitter.getOAuthRequestToken(callbackURL);
        return requestToken;
    }

    /**
     * Twitter認証のCallback処理を受け取る
     */
    public AccessToken callback(RequestToken requestToken, String verifier) throws TwitterException {
        return twitter.getOAuthAccessToken(requestToken, verifier);
    }
}

ログイン開始の処理です。
カンバセーションを開始してIDを取得するのがポイントです。
また、RequestTokenをカンバセーションにアウトジェクトしてコールバック時に備えます。
RequestTokenをカンバセーションに入れているのは、コールバックしたときに使いたいけれども、
セッションに入れても今後使わないだろうという推測からです。
Weld(CDIの実装)では現在カンバセーション開始した後のリダイレクトに問題があるようなので無理やりリダイレクトをかける必要があります。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core"
      xmlns:ui="http://java.sun.com/jsf/facelets">

    <h:form>
        <fieldset>
            <legend>ログイン</legend>
            <h:commandButton value="ログイン" action="#{loginAction.login()}" />
        </fieldset>
    </h:form>
</html>
@Named
@ConversationScoped
public class LoginAction implements Serializable {

    @Inject
    private TwitterOAuthService twitterLoginSrv;
    @Produces
    private RequestToken requestToken;
    @Inject
    private Conversation conversation;

    public void login() throws TwitterException, IOException {
        
        conversation.begin();
        
        ExternalContext extCtxt = FacesContext.getCurrentInstance().getExternalContext();
        
        String callbackURL = extCtxt.getRequestScheme()
                + "://"
                + extCtxt.getRequestServerName()
                + ":"
                + extCtxt.getRequestServerPort()
                + extCtxt.getRequestContextPath()
                + "/faces/callback.xhtml?cid=" + conversation.getId();

        requestToken = twitterLoginSrv.getRequestToken(callbackURL);

        // カンバセーションを開始すると外部サイトのリダイレクトがおかしくなる
        // https://issues.jboss.org/browse/WELD-1044
        // https://community.jboss.org/thread/180205
        // 
        // extCtxt.redirect(requestToken.getAuthenticationURL());

        Object obj = extCtxt.getResponse();
        if (obj instanceof HttpServletResponse) {
            HttpServletResponse response = (HttpServletResponse) obj;
            // ExternalContext.redirectと同じ結果になる。
            //response.sendRedirect(response.encodeURL(requestToken.getAuthenticationURL()));
            response.setHeader("Location", response.encodeRedirectURL(requestToken.getAuthenticationURL()));
            response.setStatus(HttpServletResponse.SC_SEE_OTHER);
            response.getWriter().flush();
        }
    }
}

コールバックを受け取る処理です。
ポイントはviewParamを用いてGETのパラメータを取り出し、preRenderViewで処理する点です。
また、ログイン開始と同じカンバセーションになるため、RequestTokenを取り出して使います。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core"
      xmlns:ui="http://java.sun.com/jsf/facelets">
    
    <h:head>
        <f:metadata>
            <f:viewParam name="#{callbackAction.oauthVerifierName}"
                         value="#{callbackAction.oautuVerifierValue}"
                         required="true"
                         requiredMessage="Error"/>
            <f:event type="preRenderView"
                     listener="#{callbackAction.callback()}"/>
        </f:metadata>
    </h:head>
    <h:body>
    </h:body>
</html>
@Named
@ConversationScoped
public class CallbackAction implements Serializable {

    @Inject
    private TwitterOAuthService twitterLoginSrv;
    @Inject
    private RequestToken requestToken;
    private String oauthVerifierName = "oauth_verifier";
    private String oautuVerifierValue;
    @Inject
    private Conversation conversation;

    public void callback() throws TwitterException {
        twitterLoginSrv.callback(requestToken, getOautuVerifierValue());
        conversation.end();
    }

    public String getOauthVerifierName() {
        return oauthVerifierName;
    }

    public String getOautuVerifierValue() {
        return oautuVerifierValue;
    }

    public void setOautuVerifierValue(String oautuVerifierValue) {
        this.oautuVerifierValue = oautuVerifierValue;
    }
}

あとはbeans.xmlやtwitter4j.propertiesに注意。