実装に苦労したのでメモ。
- Public なページと Secure なページを同一モジュールで実装
- Session 管理は Spring Security 任せ
org.springframework.security.web.csrf.CsrfFilter
を使用- spring-framework-4.1.4 と併用
- XML で Bean 定義
<sec:http pattern="/public/**" security="none"/>
公開ページに関しては上記のように設定し、Spring Security の Filter Chain の対象外としたいが、 対象外にすると Spring Security 管理の認証情報にアクセスできない。
例えば、公開ページであってもヘッダー領域の Login・Logout ボタンの表示判定などには Spring 管理 の認証情報へのアクセスが必要になるが、その場合は必ず Filter Chain の対象とする。
Filter Chain の対象とすることで発生する Session 関連の問題に関しては後述する。
<sec:logout logout-url="/logout" delete-cookies="JSESSIONID"/>
上記のようにログアウト時に Cookie から JSESSIONID を削除する設定を行っても、登録時と Path が異なるため削除されない。
Spring Security が Session を開始する際に Cookie に設定する JSESSIONID のパスは、request.getContextPath() + "/"
であるのに
対して、org.springframework.security.web.authentication.logout.CookieClearingLogoutHandler
が設定するパスは
request.getContextPath()
である。
また、セッション管理を Spring Security へ任せている場合は、 ログイン画面表示時点でセッションが開始され CSRF 対策のトークンが発行される。
従って、ログイン画面表示後にヘッダーメニュー等から公開画面へ戻った場合には、 Cookie に JSESSIONID が残ったままとなり画面遷移時に意図しない Session Timeout が発生してしまう。
特定条件下では Cookie から JSESSIONID を削除する Custom Filter を作成し、Filter Chain の最後に追加する。
<sec:custom-filter ref="cookieClearingFilter" after="FILTER_SECURITY_INTERCEPTOR"/>
<bean id="cookieClearingFilter" class="org.minazou67.samples.filter.CookieClearingFilter"/>
Custom Filter では、ログイン画面や認証済みの場合は Cookie 削除の対象外とする必要がある。
public class CookieClearingFilter extends GenericFilterBean {
private String loginUrl = "/login";
public void setLoginUrl(final String loginUrl) {
this.loginUrl = loginUrl;
}
@Override
public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain)
throws IOException, ServletException {
final HttpServletRequest httpRequest = (HttpServletRequest) request;
final HttpServletResponse httpResponse = (HttpServletResponse) response;
if (!(isClearing(httpRequest, httpResponse))) {
chain.doFilter(request, response);
return;
}
final Cookie cookie = new Cookie("JSESSIONID", null);
cookie.setPath(httpRequest.getContextPath() + "/");
cookie.setMaxAge(0);
httpResponse.addCookie(cookie);
chain.doFilter(request, response);
}
protected boolean isClearing(final HttpServletRequest request, final HttpServletResponse response) {
if (request.getRequestURI().equals(request.getContextPath() + loginUrl)) {
return false;
}
if ((SecurityContextHolder.getContext() != null)) {
final Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if ((auth != null) && auth.isAuthenticated()) {
if (!(auth.getName().equals("anonymousUser"))) {
return false;
}
}
} else {
return false;
}
if (request.getRequestedSessionId() == null) {
return false;
}
return true;
}
}
<sec:session-management invalid-session-url="/error/invalidSession"/>
上記のように invalid-session-url
を設定した場合は、セッションが不当な場合に指定 URL へ遷移する。
Session Timeout のチェック処理を Spring Security に委ねる場合に設定する。
<sec:logout logout-url="/logout" invalidate-session="true"/>
上記のように invalidate-session
に true
を設定するとログアウト処理時にセッションが破棄されるが、
ログアウト処理後にリダイレクトされるページが Filter Chain の対象である場合に
org.springframework.security.web.session.SessionManagementFilter
で invalid として処理されてしまう。
<sec:logout logout-url="/logout" invalidate-session="false"/>
上記のようにログアウト時にセッションを破棄しないようにするか、 エラー時の遷移先を Filter Chain の対象外にするかしか解決策はない。
<sec:session-management>
<sec:concurrency-control expired-url="/error/invalidSession" max-sessions="1" error-if-maximum-exceeded="true"/>
</sec:session-management>
上記のように error-if-maximum-exceeded
に true
を設定すると、
ユーザ毎の最大セッション数を超えた場合にエラーが発生するが、
通常の認証エラー(ユーザ名、パスワード不正)との区別がつかない。
独自の AuthenticationFailureHandler を作成し登録する。
<sec:form-login login-page="/login" authentication-failure-handler-ref="authenticationFailureHandler"/>
<bean id="authenticationFailureHandler" class="org.minazou67.samples.handler.CustomAuthenticationFailureHandler">
<constructor-arg value="/login"/>
</bean>
引き数の org.springframework.security.core.AuthenticationException
から例外の発生原因を特定し、
エラーメッセージを出し分けする。
public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
private String defaultFailureUrl;
public CustomAuthenticationFailureHandler(final String defaultFailureUrl) {
this.defaultFailureUrl = defaultFailureUrl;
}
@Override
public void onAuthenticationFailure(final HttpServletRequest request, final HttpServletResponse response,
final AuthenticationException exception) throws IOException, ServletException {
if (exception.getClass().isAssignableFrom(BadCredentialsException.class)) {
setDefaultFailureUrl(defaultFailureUrl + "?badCredentials=true");
} else if (exception.getClass().isAssignableFrom(SessionAuthenticationException.class)) {
setDefaultFailureUrl(defaultFailureUrl + "?maximumSessions=true");
}
super.onAuthenticationFailure(request, response, exception);
}
}