【Spring MVC + Spring Security】認可エラーをハンドリングする方法

概要

認可エラーをハンドリングする方法についてまとめた。

 

前提

以下の記事の続きとなる。

あわせて読みたい

概要 特定のURLに対して認可ポリシーを定義して、アクセス制御を行う方法についてまとめた。   前提 以下の記事の続きとなる。

【Spring MVC + Spring Security】認可設定(アクセス制御)を行う方法

 

認可エラーハンドリング

認可ポリシーで許可されていないURLにアクセスすると、認可エラーがスローされる。
例えば、「/admin/*」へのアクセスが許可されていないユーザーが、このURLへアクセスすると認可エラーが発生する。

認可エラーが発生した場合、Spring Security内部では以下の流れで処理を行う。

【Spring MVC + Spring Security】認可エラーをハンドリングする方法_アーキテクチャ
▲認可エラーがスローされた場合、AccessDeniedHandlerに処理が委譲される

 

① AuthorizationManagerにてアクセス権がないと判定された場合、認可エラー(AccessDeniedException)がスローされる
② ExceptionTranslationFilterにてキャッチしてAccessDeniedHandlerに処理を委譲する
③ AccessDeniedHandler(デフォルトはAccessDeniedHandlerImpl)が呼ばれ、レスポンスを返却する

 

 

実装

認可エラー画面に遷移させるAccessDeniedHandlerを実装して、ExceptionTranslationFilterに紐づける。

 

AccessDeniedHandlerの実装

認可エラーとなった場合、認可エラー画面にリダイレクトさせる。

 

CustomAccessDeniedHandler.java


package com.example.prototype.biz.security.handler;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;

public class CustomAccessDeniedHandler implements AccessDeniedHandler {
    /** ロガー */
    private static final Logger logger = LoggerFactory.getLogger(CustomAccessDeniedHandler.class);
    
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response,
            AccessDeniedException accessDeniedException) throws IOException, ServletException {
        
        logger.debug("認可エラー");
        response.sendRedirect(request.getContextPath() + "/system/error?code=403");
    }

}

 

public class CustomAccessDeniedHandler implements AccessDeniedHandler {

AccessDeniedHandlerを実装したクラスを作成する。
尚、DIを行わない場合はコンポーネント化不要。

 

@Override
public void handle(HttpServletRequest request, HttpServletResponse response,

ExceptionTranslationFilterにて呼ばれるhandleメソッドをオーバーライドする。

 

response.sendRedirect(request.getContextPath() + “/system/error?code=403”);

認可エラー時のリダイレクト処理を実装する。

 

ExceptionTranslationFilterへ紐づけ

カスタマイズしたAccessDeniedHandlerを、ExceptionTranslationFilterに紐づける。

 

SecurityConfig.java


package com.example.prototype.biz.security.config;

import java.util.Arrays;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.logout.CompositeLogoutHandler;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import com.example.prototype.biz.security.handler.CustomAccessDeniedHandler;
import com.example.prototype.biz.security.handler.LoginFailureHandler;
import com.example.prototype.biz.security.handler.LoginSuccessHandler;
import com.example.prototype.biz.security.handler.LogoutSuccessHandler;

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Autowired
    private LoginSuccessHandler loginSuccessHandler;
    
    @Autowired
    private LoginFailureHandler loginFailureHandler;
    
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    
    @Bean
    public UserDetailsService userDetailsService(PasswordEncoder encoder) {
        UserDetails disabledUser = User.withUsername("user")
            .password(encoder.encode("password"))
            .roles("USER")
            .build();
        
        return new InMemoryUserDetailsManager(disabledUser);
    }
    
    @Bean
    public DaoAuthenticationProvider authenticationProvider(MessageSource messageSource,
            PasswordEncoder passwordEncoder) {
        var provider = new DaoAuthenticationProvider();
        provider.setUserDetailsService(userDetailsService(passwordEncoder));
        provider.setPasswordEncoder(passwordEncoder);
        provider.setMessageSource(messageSource);
        return provider;
    }
    
    @Bean
    public LogoutHandler compositeLogoutHandler() {
        List<LogoutHandler> handlers = Arrays.asList(
                new SecurityContextLogoutHandler(),
                new LogoutSuccessHandler()
            );

        return new CompositeLogoutHandler(handlers);
    }
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                // CSRF機能を無効
                .csrf(csrf -> csrf.disable())

                // 認可ポリシー定義
                .authorizeHttpRequests(auth -> auth
                        // 認証不要のURL
                        .requestMatchers(
                                new AntPathRequestMatcher("/authentication"),
                                new AntPathRequestMatcher("/logout")
                                ).permitAll()

                        // ロールベースの認可
                        .requestMatchers(new AntPathRequestMatcher("/user/**")).hasAnyRole("USER", "ADMIN")
                        .requestMatchers(new AntPathRequestMatcher("/admin/**")).hasRole("ADMIN")

                        // 権限ベースの認可
                        .requestMatchers(new AntPathRequestMatcher("/report/list")).hasAuthority("READ_PRIVILEGE")
                        .requestMatchers(new AntPathRequestMatcher("/user/delete")).hasAuthority("DELETE_PRIVILEGE")

                        // その他は認証済みなら許可
                        .anyRequest().authenticated())
                
                // ログイン機能定義
                .formLogin(form -> form
                        .loginPage("/authentication")
                        .loginProcessingUrl("/authentication/process")
                        .usernameParameter("loginId")
                        .passwordParameter("pass")
                        .successHandler(loginSuccessHandler)
                        .failureHandler(loginFailureHandler))

                // ログアウト機能定義
                .logout(logout -> logout
                        .logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
                        .addLogoutHandler(compositeLogoutHandler())
                        .logoutSuccessUrl("/authentication?logout=true")
                )
                
                // 認可エラーハンドリング
                .exceptionHandling(ex -> ex 
                        .accessDeniedHandler(new CustomAccessDeniedHandler()));

        return http.build();
    }
}

 

.exceptionHandling(ex -> ex.accessDeniedHandler(new CustomAccessDeniedHandler())

ExceptionTranslationFilterが内部で保持するAccessDeniedHandlerを、作成したクラスで上書きしている。
これにより、ExceptionTranslationFilterにて認可エラーをキャッチすると、CustomAccessDeniedHandlerが呼ばれる。

 

まとめ

 

☑ 認可ポリシーで許可されていないURLにアクセスすると、認可エラー(AccessDeniedException)がスローされる

☑ この例外はExceptionTranslationFilterによって捕捉され、内部のAccessDeniedHandlerに処理が委譲される

☑ 認可エラーをハンドリングしたい場合、AccessDeniedHandlerを実装してExceptionTranslationFilterに紐づける

 

スポンサーリンク