SAStrutsでログイン確認インターセプタを使う

Webアプリケーションでありがちな「ログイン済みか確認する」ための処理を、S2AOPを使って組み込んでみた。

やりたいことは、「Actionクラスの@Executeなメソッドが呼ばれたとき、ログイン済みかどうかを確認し、ログインしていなければログイン画面にリダイレクトする」である。この処理を全てのActionに書いて回るのは当然ながら面倒なので、AOPで処理を差し込んでしまいたいというわけ。
通常、ログインしているかどうかはHttpSessionが特定の属性値を持っているかで判断する。そこで、次のようなインターセプタを定義する。パッケージを{root}.interceptorとしておけば、勝手にコンポーネントとして定義されるので楽。

追記: セッションの使い方が間違っているようです。Seasar 2.4.34以前では、HotDeploy時にClassCastExceptionが投げられる場合があります。こちらの記事を参照してください。

package myapp.root.interceptor;

// import省略

public class LoginConfirmInterceptor extends AbstractInterceptor {

    public Object invoke(MethodInvocation invocation) throws Throwable {
        return isLoggedIn() ? invocation.proceed() : "/login/index?redirect=true";
    }

    private boolean isLoggedIn() {
        HttpSession session =
            (HttpSession) SingletonS2ContainerFactory
                .getContainer()
                .getExternalContext()
                .getSession();
        
        LoginDto dto = (LoginDto) session.getAttribute("loginDto");
        
        return (loginDto != null && loginDto.id != null);
    }
}

これをcustomizer.diconでActionクラスに適用する。ただし、ログイン画面用のActionは除外。

  <component name="actionCustomizer" class="org.seasar.framework.container.customizer.CustomizerChain">
    ...
    <initMethod name="addCustomizer">
      <arg>
        <component class="org.seasar.framework.container.customizer.AspectCustomizer">
          <property name="interceptorName">"loginConfirmInterceptor"</property>
          <pointcut>".*"</pointcut>
          <initMethod name="addIgnoreClassPattern">
            <arg>"myapp.root.action"</arg>
            <arg>"LoginAction"</arg>
          </initMethod>
        </component>
      </arg>
    </initMethod>
  </component>

この定義では少し困る。というのも、このポイントカットだとActionが持つ全てのメソッド…例えば、Object#toString()等にもアスペクトしてしまう。

ログイン確認したいメソッドの条件は、「@Executeアノテーションが付与されたメソッド」である。しかし、S2AOPでは「@Executeが付与されているメソッドのみアスペクトする」といった定義はできない(と思う)。アスペクト対象の判断基準はメソッド名だ。これは「命名パターンよりもアノテーションを選ぶ*1」というJava5以降のスタイルに合わない。

さて、どうしようか。ポイントカットに定義されそうなメソッド名を延々とカンマでつないでいく手もあるが、保守性は最悪だ。

ここで発見。

[CONTAINER-128] S2AOPで,pointcutが指定されなかった場合は Object.class に属していない public なメソッドが対象となるようにしました. - The Seasar Foundation Issues (Deprecated)

Actionに定義するpublicメソッドなんて、ほとんど@Executeが付与されているだろう。これはかなりやりたいことに近づく。

そこで、ポイントカットの定義を無くしてみた。

  <component name="actionCustomizer" class="org.seasar.framework.container.customizer.CustomizerChain">
    ...
    <initMethod name="addCustomizer">
      <arg>
        <component class="org.seasar.framework.container.customizer.AspectCustomizer">
          <property name="interceptorName">"loginConfirmInterceptor"</property>
          <initMethod name="addIgnoreClassPattern">
            <arg>"myapp.root.action"</arg>
            <arg>"LoginAction"</arg>
          </initMethod>
        </component>
      </arg>
    </initMethod>
  </component>

そして、念のためインターセプタ側にもう一工夫しておく。@Executeが付与されていないメソッドについては、ログイン確認処理を行わないようにしておくのである。

public class LoginConfirmInterceptor extends AbstractInterceptor {

    public Object invoke(MethodInvocation invocation) throws Throwable {
        return (!isExecuteMethod(invocation) || isLoggedIn())
            ? invocation.proceed() : "/login/index?redirect=true";
    }

    private boolean isExecuteMethod(MethodInvocation invocation) {
        return invocation.getMethod().isAnnotationPresent(Execute.class);
    }
    
    private boolean isLoggedIn() {
        HttpSession session =
            (HttpSession) SingletonS2ContainerFactory
                .getContainer()
                .getExternalContext()
                .getSession();
        
        LoginDto dto = (LoginDto) session.getAttribute("loginDto");
        
        return (loginDto != null && loginDto.id != null);
    }
}

実質、アスペクト先をカスタマイザとコードの二カ所に分散して定義しているようなものなので、スマートとは言い難い解決策である。しかし、とりあえず問題無く動くようなので、これでよしとする。問題になるようならば、アノテーションの恩恵の一部を捨てることになるが、メソッドのネーミングルールを定めるしかないだろう。