Spring Framework Advent Calendar 2011 part.7 - Spring Security で認証成功時に条件によって遷移先を変える

Spring Framework Advent Calendar の 7 日目です。今日はふたたび Spring Security の話です。小出しにしているような感もありますが、気にせずいきましょう。


以前 Spring Security でアカウントロック機構を実現する - 倖せの迷う森 にて、Spring Security では認証の成功や失敗に対応するイベントハンドラがあり、それらのイベントに対応する Listener を書いて Bean 登録しておけば自動的にそれが呼び出される、という話をしました。
さて、あなたが現在作成中の Web システムにこのような認証に関する要件が与えられたとします。

  • 認証はIDとパスワードで行う
  • パスワードには有効期限がある
  • 有効期限を過ぎたパスワードでログインした場合、パスワード強制変更画面に飛ばす


以前紹介した ApplicationListener を用いる手法では、ログイン後に遷移するURLを条件によって変更することはできませんので、このような要件を解決することはできませんでした。ApplicationListener は、認証イベントに対応して実行される「裏方」のようなもので、HTTP に関する操作(リダイレクトとか)というのは想定されていません。
ああ、認証成功時に遷移するURLに対して手を加えられるような方法があれば...そう思いますよね。まさにその部分を担当している Spring Security のインターフェースがあります。そう、AuthenticationSuccessHandler です。この Handler は、名前の通り認証成功時に実行される、遷移先のURLをコントロールするためのハンドラです。インターフェース宣言されているメソッドのシグニチャ

public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
        Authentication authentication) throws ServletException, IOException

であることからも分かる通り、HttpRequest や HttpResponse を操作するためにあるインターフェースであることが分かると思います。このインターフェースを適切に実装すれば、上記要件を満たすことができそうですね。
それでは実装例を見てみましょう。

import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;

public class PasswordExpiredCheckAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {

    private String redirectUrl;

    public void setRedirectUrl(String redirectUrl) {
        this.redirectUrl = redirectUrl;
    }
    
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
            Authentication authentication) throws ServletException, IOException {

        String userId = authentication.getName();

        if (isPasswordExpired(userId)) {
            RedirectStrategy redirectStrategy = getRedirectStrategy();
            redirectStrategy.sendRedirect(request, response, redirectUrl);
            return;
        }
        super.onAuthenticationSuccess(request, response, authentication);
    }
    
    private boolean isPasswordExpired(String userId) {
        // implementation...
    }
}


今回は AuthenticationSuccessHandler の実装のひとつである SavedRequestAwareAuthenticationSuccessHandler をさらに拡張する方式を採用しています。SavedRequestAwareAuthenticationSuccessHandler は、認証のために Spring Security が割り込んだ際の元のリクエストを、認証成功時に復元するというハンドラです*1
この拡張された AuthenticationSuccessHandler を利用するには、Spring Security の Bean 定義ファイルに以下のように書きます。

    ...
    <sec:http auto-config="true">
        ...
        <sec:form-login
            authentication-success-handler-ref="passwordExpiredCheckAuthenticationSuccessHandler" />
        ...
    </sec:http>
    <bean id="passwordExpiredCheckAuthenticationSuccessHandler"
        class="org.example.app.PasswordExpiredCheckAuthenticationSuccessHandler">
        <property name="redirectUrl" value="/expired" />
        <property name="alwaysUseDefaultTargetUrl" value="true" />
        <property name="defaultTargetUrl" value="/" />
    </bean>
    ...


このように、新しく作成したハンドラを Bean 登録した上で、それを の authentication-success-handler-ref 属性で参照します。
以前紹介した ApplicationListener を用いる手法と今回紹介した手法の使い分けとしては、

  • 認証後の遷移先を制御する目的なら AuthenticationSuccessHandler
  • 認証成功にトリガーして、裏で何かする (ロギングなど) なら ApplicationListener

このように意識しておけばよいでしょう。

*1:元リクエストは通常、Session スコープに保持されています。