Block Rockin’ Codes

back with another one of those block rockin' codes

SAStrutsでセッションを使ったログインと認証

認証の方法は悩みがちなポイントだと思います。コンテナ等の実装も含めると手段は色々あるし、一言に認証といっても、色々な業務ロジックが絡んでくることも多いからでしょうか。


今回はSAStrutsで、sessionとAOPを使ったスタンダードな方法を実装しました。
仕組みはいたってシンプルで、何らかのロジックで認証した後、ID等のデータをセッションに格納して、その有無でログイン済みかを確認するというものです。ログアウトはそのセッションを廃棄することになります。Webアプリケーションでは王道の方法だと思います。


この場合、認証のチェックが必要な場面で同じ処理が必要になるので、SAStrutsではセッションのチェックはメソッドを分けて、AOPでアクションに適応します。
今回は、全体的にログインしっぱなしでいて欲しいので、LoginAction以外では全てのアクションで確認します。
これにより、どのページにアクセスしても、セッションが無ければ必ずLoginページに飛びます。
またLogout処理はセッションの破棄になります。これはActionにアノテーションを付けるだけです。


備忘録を兼ねてサンプルを、今回はSAStruts + S2JDBCDolteng プロジェクトが土台になっています。

認証データの保持クラス


セッションに保持したいdto
このdtoが@Componentのアノテーションでセッションに自動で保持されるようになっています。
ここには、IDや認証中に使用するデータ等を入れます。

package sample.dto;

import java.io.Serializable;

import org.seasar.framework.container.annotation.tiger.Component;
import org.seasar.framework.container.annotation.tiger.InstanceType;

@Component(instance = InstanceType.SESSION)
public class UserDataDto implements Serializable {
	private static final long serialVersionUID = 1L;

	public String userId;
}

インターセプタ


対象クラスのなかで@Executeアノテーションがついたメソッドの実行時に、SessionのDtoの有無(つまり認証済みかどうか)を確認するインターセプタ。

package sample.interceptor;

import javax.annotation.Resource;

import org.aopalliance.intercept.MethodInvocation;
import org.seasar.framework.aop.interceptors.AbstractInterceptor;
import org.seasar.struts.annotation.Execute;

import sample.dto.UserDataDto;


public class LoginConfirmInterceptor extends AbstractInterceptor {
	private static final long serialVersionUID = 1L;

	/**
	 * セッションに保持されているデータです。
	 */
	@Resource
	protected UserDataDto userDataDto;

	/**
	 * AbstractInterceptorを継承する際に、実装する必要のあるメソッド。
	 * 割り込ませる処理を記述。
	 */
	@Override
	public Object invoke(MethodInvocation invocation) throws Throwable {
		// 両方の条件を満たしていない場合、Loginページへ飛ばす。
		return (!isExecuteMethod(invocation) || isLoggedIn()) ? invocation
				.proceed() : "/login/";
	}

	/**
	 * 実行されたActionに@Executeがついていたかどうか。
	 * @param invocation
	 * @return アノテーションがついていればtrue
	 */
	private boolean isExecuteMethod(MethodInvocation invocation) {
		return invocation.getMethod().isAnnotationPresent(Execute.class);
	}

	/**
	 * セッション上にDtoがあるか、あった場合その中にuserIDは保持されているか。
	 * @return 上記の条件を両方満たしていればtrue
	 */
	private boolean isLoggedIn() {
		return (userDataDto != null && userDataDto.userId != null);
	}
}

このインターセプタを、狙ったメソッドの実行時に実行されるように登録します。

AOPの登録


作成したインターセプタをcustomizer.diconに登録します。ここで対象とするアクションを"LoginAction以外"に設定することで、今回の目的が果たせます。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE components PUBLIC "-//SEASAR//DTD S2Container 2.4//EN"
	"http://www.seasar.org/dtd/components24.dtd">
<components>
  ・・・
  <!-- actionCustomizerに対して処理を足して行く-->
  <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>

				<!-- 処理の対象外ににしたいAction(これ以外が対象になる) -->
				<initMethod name="addIgnoreClassPattern">
					<arg>"sample.action"</arg>
					<arg>"LoginAction"</arg>
				</initMethod>

				<!-- 対象にしたいクラスの設定はこちら -->
				<!--
				<initMethod name="addClassPattern">
					<arg>"sample.action"</arg>
					<arg>"OtherAction"</arg>
				</initMethod>
				-->
			</component>
		</arg>
	</initMethod>
	<!-- 追加設定ここまで -->
  </component>
  <component name="serviceCustomizer" class="org.seasar.framework.container.customizer.CustomizerChain">
	・・・
  </component>
</components>

LoginAction


以上を使って簡単なLoginを作ってみました。
Login.jsp等でIDとパスワードの入力をLoginForm.javaなどで受け取ったとします。
LoginAction.javaではそれを元にDB等への問い合わせ等の認証処理をします。
認証が成功した場合は、最初に作ったUserDataDto.javaにそれを保持します。
UserDataDto.javaにデータを入れるだけで、自動でセッションに保持されます。

package sample.dto;

import java.io.Serializable;

import org.seasar.framework.container.annotation.tiger.Component;
import org.seasar.framework.container.annotation.tiger.InstanceType;

@Component(instance = InstanceType.SESSION)
public class UserDataDto implements Serializable {
	private static final long serialVersionUID = 1L;

	public String userId;
}

これでセッションへの保持は終わりです。
認証のロジックはLoginService等に切り出した方がいいでしょう。

セッションの破棄


今回は、"UserDataDtoがセッションにある"かつ"UserDataDto.userIDがnullではない"が認証されている条件なので、このどちらかが満たされない状態は認証が強制されます。
以下はUserDataDtoを丸々破棄することでログアウトを実現するLogoutActionの例です。

package sample.action;

import org.seasar.framework.aop.annotation.RemoveSession;
import org.seasar.struts.annotation.Execute;

public class LogoutAction {

	@Execute(validator = false)
	@RemoveSession(name = "userDataDto")
	public String index() {
        return "/";
	}
}

ブラウザを閉じたり、サーバを再起動したらもちろんセッションは破棄されます。

注意


SAStrutsのページにもありますが

Dtoをセッションに保存している場合に、DtoのHOT deployが効かない場合があります。
Tomcatを使っているなら、context.xmlをリンクの指示に従って修正してください。 


とあります。リンク先はhttp://sastruts.seasar.org/fileReference.html#context:こちらです。

原因は

Tomcatを使っていて、ActionFormや Dtoをセッションに格納するようにしている場合に、HOT deployが効かないことがあります。
これは、Tomcatがセッション情報をシリアライズし、 Seasar2が関与できないところでデシリアライズすることが原因です。 


設定はtomcatのconf/context.xmlのManagerをコメントアウトするだけ。

    <!-- Uncomment this to disable session persistence across Tomcat restarts -->
    <Manager pathname="" />


今回Dtoをまさしくセッションで管理しているので、この対応が必要になると思います。
セッション情報はメモリ上に格納されますが、そこがいっぱいになったりすると自動でディスク等に書き込まれるような実装が多いです。なぜならセッション数≒ユーザ数が増えるとメモリが圧迫されることがあるからです。

この対応はセッションの情報を永続化(要するにディスクに書き込む)させなくすることで、全てSeaser2が管理できるようにするということのようです。逆を言うとメモリの量とセッションオブジェクトのサイズの見積もり等には設計側で気をつける必要がありそうです。(そうでなくても気をつけるべきではありますが。)


今回の認証の特徴

  • セッションの値の有無で調べる。WebAPではかなりスタンダードな方法なので分かりやすい。
  • 実装自体は難しくない。
  • 認証はLoginService等に切り出すので、保持のロジックと切り離されている。
  • 権限等の細かな設定はUserDataDto等に工夫をすれば出来るかもしれないが、多分面倒くさい。
  • Actionごとに細かく設定を変えたい場合も多分面倒くさい。
  • その意味で複雑な認証には向かないかも。
  • セッションオブジェクトの永続化層の書き込みには注意する。(Dtoのサイズとユーザ数)

ソース


今回のソースはこちら(mainのみ)
jxck / SAStruts Login Sample / source / — Bitbucket