SAStrutsでアノテーションを利用したログイン検証

SAStrutsは内部でSeasar2を利用しているので、独自のインターセプタを定義してクラスに織り込むことが出来る(AOP)。Webアプリケーションといえば、ログイン検証はおなじみの処理だが、これはインターセプタを使って実現することができる。

以下は、Actionクラスにアノテーションを指定してログイン検証処理を行うサンプルである。ちなみに検討課題が無いわけではない。

ログイン情報格納用クラス

@Component(instance = InstanceType.SESSION)
public class LoginDto implements Serializable {

    private static final long serialVersionUID = 1L;

    public Integer id;
}

セッションで管理するのでInstanceType.SESSIONで。

ログイン検証指定アノテーション

Actionクラスに指定し、その中の全ての@Executeメソッドで検証処理が行われる仕様にする。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface Authentication {}

実行メソッド単位で指定できるようにすることもできるが、それは少々面倒(=開発でミスが起きやすい)なので、1ユースケースにあたるAction単位で指定できるようにした。1ユースケース中で、検証したい場合としたくない場合があるというのはあまり考えられないが、その特殊なケースが発生したら、メソッドに@NotAuthenticationみたいなアノテーションつけて対応するという逃げ道はある。

ログイン検証インターセプタ

実際に検証処理を行うインターセプタ。

package levelup.aop;

public class AuthenticationInterceptor extends AbstractInterceptor {

    private static final long serialVersionUID = 1L;

    @Resource
    protected LoginDto loginDto;

    public Object invoke(MethodInvocation invocation) throws Throwable {

        if (isTargetMethod(invocation) && !isLogin()) {
            return "/login?redirect=true";
        }
        return invocation.proceed();
    }

    private boolean isTargetMethod(MethodInvocation invocation) {

        Method method = invocation.getMethod();
        Class<?> clazz = invocation.getThis().getClass();

        Execute execute = method.getAnnotation(Execute.class);
        Authentication auth = clazz.getAnnotation(Authentication.class);

        return execute != null && auth != null;
    }

    private boolean isLogin() {
        return loginDto.id != null;
    }
}

MethodInterceptorインターフェースを実装してもよかったけど、今回はAbstractInterceptorを継承して実装。LoginDtoがログイン情報で、これはDIコンテナにより自動的にセットされる(型がインターフェースではないので@Resouceは必須)。検証対象のメソッドで、未ログインの場合はログインActionにリダイレクトし、実行メソッドの呼出は行わない。飛ばしたい先が変わるということはあまり考えられないけど、心配ならreturnPathみたいなプロパティを用意しておいて、diconファイルで指定するのも手。

app.dicon

インターセプタの登録。

<components>
        ...
	<component name="authenticationInterceptor" instance="request"
		class="sample.aop.AuthenticationInterceptor" />
</components>

S2Containerのデフォルトのインスタンスは"singleton"だが、リクエスト単位で処理するので"request"を指定。これを忘れると、LoginDtoがインジェクションされない(自分より短いライフサイクルのコンポーネントはインジェクション出来ない)。

customizer.dicon

Actionにインターセプタを適用。

<components>
	<component name="actionCustomizer"
	    class="org.seasar.framework.container.customizer.CustomizerChain">
		...
		<initMethod name="addAspectCustomizer">
			<arg>"authenticationInterceptor"</arg>
			<arg>true</arg>
		</initMethod>
		...

addAspectCutomizerの第2引数は、インスタンスがSingletonじゃない場合はtrueにしなきゃならないらしい。

コンポーネントに適用するインターセプタのインスタンス属性がsingleton以外の場合は、 useLookupAdapterプロパティをtrueに設定します。 これにより、 コンポーネントのメソッドが呼び出される度に、 コンテナからインターセプタのインスタンスをルックアップするようになります。

とのことだが、requestなインスタンスならいらないような気もする・・・どうなんだろ?

Actionに指定

こうやって使う。

@Authentication
public class IndexAction {
    ...
    @Execute(validator = false)
    public String index() {
        ...
    }
    ...
}

実際の処理ではログイン処理なんか一切気にしていないのが、いかにもAOP

課題

ログイン検証処理よりもActionFormのバリデーションの方が先に動作してしまう。つまり、入力内容にエラーがあり、ログイン情報が切れていても、ログイン画面には飛んでくれない。

  • おいジョージ、まだ登録が終わらないのか?
  • ああ、ちょっ入力に間違いがあってな・・・よし終わっ・・・what!?
  • ん?どうしたジョージ?
  • ・・・ログイン画面に飛ばされちまったんだ。

なんて事態も考えられなくはない。

まとめ

  • 特殊なケースを妥協するなら、AOPのログイン認証も悪くない

次回は、これのユニットテストのやり方を書きたいと思う。