はじめに
Spring Boot + Spring Security使用時のSessionTimeout対応の最後に、「CSRF対策が有効の場合、POST時にSessionTimeoutしているとHTTP Status:403 Forbiddenが発生してしまう問題がある。」と記載した。
今回はこの問題の対応方法を記載し、Spring SecurityのJavaConfigの完成形を作る。
CSRF対策のせいでHTTP Status:403 Forbiddenが起こる原因
まずこの問題が起こる原因は、CSRF対策の仕組みが、リクエストパラメータで送られるCSRF TokenとSessionに保存されたCSRF Tokenを比較するというロジックであり、Sessionに依存しているから。
SessionがTimeoutによって消滅しているときにCSRF Tokenをリクエストパラメータで送っても、Sessionは既に存在していないから必ずTokenが違うということになる。
accessDeniedHandlerによる対策
対策したソースを記載する。
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.access.AccessDeniedException;
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 org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.access.AccessDeniedHandlerImpl;
import org.springframework.security.web.csrf.MissingCsrfTokenException;
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())
// csrfはsessionがないと動かない。SessionTimeout時にPOSTすると403 Forbiddenを必ず返してしまうため、
// MissingCsrfTokenExceptionの時はリダイレクトを、それ以外の時は通常の扱いとする。
.accessDeniedHandler(accessDeniedHandler())
;
}
@Bean
AuthenticationEntryPoint authenticationEntryPoint() {
return new SessionExpiredDetectingLoginUrlAuthenticationEntryPoint("/login");
}
@Bean
AccessDeniedHandler accessDeniedHandler() {
return new AccessDeniedHandler() {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
if (accessDeniedException instanceof MissingCsrfTokenException) {
authenticationEntryPoint().commence(request, response, null);
} else {
new AccessDeniedHandlerImpl().handle(request, response, accessDeniedException);
}
}
};
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
}
前回との差分は1点。
- exceptionHandling().accessDeniedHandler()にAccessDeniedExceptionが発生したときの処理を書いた無名クラスを設定した。
発生したExceptionが、CSRF Tokenがない場合に発生するMissingCsrfTokenExceptionだった場合、SessionTimeoutであると判断してSessionExpiredDetectingLoginUrlAuthenticationEntryPointを実行する。
SessionExpiredDetectingLoginUrlAuthenticationEntryPointではSessionが当然Invalidであると判断するので、/login?timeoutにリダイレクトしてくれる。