Spring Framework Advent Calendar 2011 part.5 - Spring Security でセッションタイムアウト時の遷移先をカスタマイズする

Spring Framework Advent Calendar の 5 日目です。今日は Spring Security の話です。なんだか Spring MVC と Spring Security の話ばかりしているような気がしますが、あまり気にしないことにします。

セッションタイムアウト時の画面遷移

ログインが必要なWebアプリケーションの開発をしていると、たいていの場合、

といった要望を受けます。
Spring Security のデフォルトの動作は、セッションタイムアウトを特別扱いはしていませんから、ログインしていない状態でのアクセスと同じように画面遷移します。この挙動を変更したい場合はどのようにすればよいでしょうか。
Spring Security 3.0 以降の話になりますが、Spring 3.0 ではログイン画面へのリダイレクトを決定する LoginUrlAuthenticationEntryPoint があります*1。この AuthenticationEntryPoint をカスタマイズすることで、デフォルトの挙動を変更することができます。以下は、セッションタイムアウト時に、timeout=true という URL パラメーターを付加したうえでログイン画面へのリダイレクトを行う例です。

public class SessionExpiredDetectingLoginUrlAuthenticationEntryPoint
        extends LoginUrlAuthenticationEntryPoint {

    @Override
    protected String buildRedirectUrlToLoginPage(
            HttpServletRequest request, HttpServletResponse response,
            AuthenticationException authException) {

        String redirectUrl = super.buildRedirectUrlToLoginPage(request, response, authException);
        if (isRequestedSessionInvalidate(request)) {
            redirectUrl += redirectUrl.contains("?") ? "&" : "?";
            redirectUrl += "timeout=true";
        }
        return redirectUrl;
    }

    private boolean isRequestedSessionInvalidate(HttpServletRequest request) {
        return request.getRequestedSessionId() != null && !request.isRequestedSessionIdValid();
    }
}


ただし、この方式では JavaScriptXMLHTTPRequest を用いて行われる通信で発生したセッションタイムアウトを検出することができません。より正確には、サーバーサイドはきちんとセッションタイムアウトを検出してエラー画面を返します。しかし、XMLHTTPRequest を用いて通信し、サーバーの応答が JSON であることを期待しているようなコードの場合はHTMLが返ってきても対応できず、おかしな動作となります。この問題を解決する方法としては Spring Securityで、セッションタイムアウト時のAjaxリクエストに対応する - penultの日記 にもあるとおり、HTTP応答コードで適切なエラーコードを返してやる方法がよいでしょう。この修正を取り込むと、最終的に以下のようなコードになりました。

public class SessionExpiredDetectingLoginUrlAuthenticationEntryPoint
        extends LoginUrlAuthenticationEntryPoint {

    private static final String AJAX_REQUEST_IDENTIFIER = "XMLHttpRequest";

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException)
            throws IOException, ServletException {

        if (AJAX_REQUEST_IDENTIFIER.equals(request.getHeader("X-Requested-With"))) {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            return;
        }

        super.commence(request, response, authException);
    }
    
    @Override
    protected String buildRedirectUrlToLoginPage(
            HttpServletRequest request, HttpServletResponse response,
            AuthenticationException authException) {

        String redirectUrl = super.buildRedirectUrlToLoginPage(request, response, authException);
        if (isRequestedSessionInvalidate(request)) {
            redirectUrl += redirectUrl.contains("?") ? "&" : "?";
            redirectUrl += "timeout=true";
        }
        return redirectUrl;
    }

    private boolean isRequestedSessionInvalidate(HttpServletRequest request) {
        return request.getRequestedSessionId() != null && !request.isRequestedSessionIdValid();
    }
}


この AuthenticationEntryPoint を利用するには、Spring Security の Bean 定義ファイルに設定が必要です。security 名前空間 タグに entry-point-ref 属性がありますので、ここに AuthenticationEntryPoint の id を記述します。もちろん、カスタマイズ済みの AuthenticationEntryPoint については新たに Bean 定義を記述する必要があります。

  <sec:http auto-config="true" entry-point-ref="sessionExpiredDetectingEntryPoint">
    <sec:intercept-url pattern="/**" access="IS_AUTHENTICATED_ANONYMOUSLY" />
    <sec:intercept-url pattern="/admin/" access="ROLE_ADMIN" />
  </sec:http>

  <bean id="sessionExpiredDetectingEntryPoint"
    class="examples.security.SessionExpiredDetectingLoginUrlAuthenticationEntryPoint">
    <property name="loginFormUrl" value="/login" />
  </bean>

まとめ

セッションタイムアウト時の画面遷移の動きをカスタマイズするのは、方法を知っていれば簡単にできるということをお話ししました。

*1:以前は AuthenticationProcessingFilterEntryPoint と呼ばれていたようです。