はじめに
Spring BootにSpring Securityを入れた時のSessionTimeoutのデフォルト挙動は、ログイン画面への自動遷移になる。
一般的な要件として、ログイン画面に遷移したときに「タイムアウトしました。」などのメッセージを表示しなければいけないような時の対応方法を記載する。
※関連ページ:Spring Boot + Spring Security使用時のCSRFとSessionTimeoutの問題
検証version
・Spring Boot 1.3.3
・Spring Security 4.0.3
参考までにView側技術
・Thymeleaf 2.1.4
・Bootstrap 3.3.6
LoginUrlAuthenticationEntryPointの拡張
org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPointというExceptionが発生したときのログイン画面へのリダイレクト用クラスをSpring Securityが用意していて、これを拡張してする。
LoginUrlAuthenticationEntryPoint
Used by the ExceptionTranslationFilter to commence a form login authentication via the UsernamePasswordAuthenticationFilter.
Holds the location of the login form in the loginFormUrl property, and uses that to build a redirect URL to the login page.
拡張方法は、リダイレクトURLを決定するメソッドbuildRedirectUrlToLoginPageをOverrideする。
@Override
protected String buildRedirectUrlToLoginPage(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) {
String redirectUrl = super.buildRedirectUrlToLoginPage(request, response, authException);
if (isRequestedSessionInvalid(request)) {
redirectUrl += redirectUrl.contains("?") ? "&" : "?";
redirectUrl += "timeout";
}
return redirectUrl;
}
private boolean isRequestedSessionInvalid(HttpServletRequest request) {
return request.getRequestedSessionId() != null && !request.isRequestedSessionIdValid();
}
処理の内容は、Sessionが無効状態になっているとき、デフォルトのリダイレクトURLの後ろにリクエストパラメータ"timeout"を付与する。
これにより、/login?timeout
というURLになる。
Ajaxの場合の対処法
しかしAjaxリクエストの場合はリダイレクトが動かないため、リクエストがAjaxかどうかを判定して、Ajaxの場合はHTTP STATUS:401 Unauthorized
を返すのみにして、JavaScript側でリダイレクトするように対応する必要がある。
まずは、リクエストがAjaxかどうかを判定するために、LoginUrlAuthenticationEntryPoint#commenceをOverrideする。
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
if ("XMLHttpRequest".equals(request.getHeader("X-Requested-With"))) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
super.commence(request, response, authException);
}
JavaScript側はjQueryを用いて書くと以下のようになる。
var ajaxUrl ="/ajax?id=1";
$.ajax({
type : "GET",
url : ajaxUrl,
statusCode: {
401: function() {
window.location.href = /*[[@{/login?timeout}]]*/"";
}
}
}).done(
function(json) {
// 通常処理
});
HTMLでの処理
login.htmlでは、 th:if="${param.timeout}"
という条件式を設定して、リクエストパラメータにtimeoutが存在するときのみ「タイムアウトしました。」という文言を表示するようにしている。
以下はlogin.htmlのBODY部。
<body>
<div class="container" layout:fragment="content">
<div th:include="common/pageheader :: pageheader('ログイン画面')"></div>
<div th:if="${param.error}" id="information" class="alert alert-danger alert-dismissible">
<button type="button" class="close" data-dismiss="alert" aria-label="閉じる"><span aria-hidden="true">×</span></button>
ユーザ名かパスワードに誤りがあります。
</div>
<div th:if="${param.logout}" id="information" class="alert alert-success alert-dismissible">
<button type="button" class="close" data-dismiss="alert" aria-label="閉じる"><span aria-hidden="true">×</span></button>
ログアウトしました。
</div>
<div th:if="${param.timeout}" id="information" class="alert alert-info alert-dismissible">
<button type="button" class="close" data-dismiss="alert" aria-label="閉じる"><span aria-hidden="true">×</span></button>
タイムアウトしました。
</div>
<form th:action="@{/login}" method="post">
<div class="form-group col-sm-2"><label class="control-label">ユーザ名</label><input type="text" class="form-control input-sm" id="username" name="username" style="ime-mode: disabled;"/></div>
<div class="form-group col-sm-2"><label class="control-label">パスワード</label><input type="password" class="form-control input-sm" id="password" name="password"/></div>
<div class="form-group col-sm-1"><label></label><input type="submit" class="btn btn-primary" id="login" value="ログイン"/></div>
</form>
</div>
</body>
Spring Securityへの設定
あとは、拡張したクラスをSpring Securityに認識させることで、SessionTimeout時のリダイレクトとリダイレクト先での文言表示が実現できる。
まず、拡張クラスSessionExpiredDetectingLoginUrlAuthenticationEntryPointの全文を以下に掲載する。
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
public class SessionExpiredDetectingLoginUrlAuthenticationEntryPoint extends LoginUrlAuthenticationEntryPoint {
public SessionExpiredDetectingLoginUrlAuthenticationEntryPoint(String loginFormUrl) {
super(loginFormUrl);
}
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
if ("XMLHttpRequest".equals(request.getHeader("X-Requested-With"))) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
super.commence(request, response, authException);
}
@Override
protected String buildRedirectUrlToLoginPage(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) {
String redirectUrl = super.buildRedirectUrlToLoginPage(request, response, authException);
if (isRequestedSessionInvalid(request)) {
redirectUrl += redirectUrl.contains("?") ? "&" : "?";
redirectUrl += "timeout";
}
return redirectUrl;
}
private boolean isRequestedSessionInvalid(HttpServletRequest request) {
return request.getRequestedSessionId() != null && !request.isRequestedSessionIdValid();
}
}
SessionExpiredDetectingLoginUrlAuthenticationEntryPointをSpring SecurityのJavaConfigで設定する。
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.AuthenticationEntryPoint;
import jp.co.sample.service.UserDetailsServiceImpl;
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
UserDetailsServiceImpl userDetailsService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/css/**").permitAll()
.antMatchers("/js/**").permitAll()
.antMatchers("/fonts/**").permitAll()
.antMatchers("/login", "/login?**").permitAll()
.antMatchers("/logout").permitAll()
.anyRequest().authenticated()
.and().formLogin()
.and().logout()
.and().exceptionHandling()
// 通常のRequestとAjaxを両方対応するSessionTimeout用
.authenticationEntryPoint(authenticationEntryPoint())
;
}
@Bean
AuthenticationEntryPoint authenticationEntryPoint() {
return new SessionExpiredDetectingLoginUrlAuthenticationEntryPoint("/login");
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
}
WebSecurityConfigクラスでのポイントは2点。
- exceptionHandling().authenticationEntryPoint()に拡張したSessionExpiredDetectingLoginUrlAuthenticationEntryPointを設定する
- /login?timeoutを認証不要のURLと認識させるため、authorizeRequests().antMatchers("/login", "/login?**").permitAll()を設定する。
よくあるSpring Securityのサンプルでは、 以下のようになっていることが多いが、authorizeRequests().antMatchers("/login", "/login?**").permitAll()を設定するため、formLogin()とlogout()の箇所でpermitAll()をせず、authorizeRequests()でpermitAll()する。
.and().formLogin()
.permitAll()
.and().logout()
.permitAll()
以上でSpring Boot + Spring Security使用時のSessionTimeout対応は完了。 ただし、CSRF対策が有効の場合、POST時にSessionTimeoutしているとHTTP Status:403 Forbiddenが発生してしまう問題がある。 CSRFとSessionTimeout問題は別ページで対応策を記載する。 Spring Boot + Spring Security使用時のCSRFとSessionTimeoutの問題