はじめに
業務でSpring Securityを使用して認証処理を実装することがあり、認証エラーが発生した時のハンドリング処理をコントローラーの処理と合わせて@ExceptionHandlerでハンドリングできるように実装したのでこちらでアウトプットしておく。
ハンドリング処理に焦点を当てて解説するため、その他の実装は最低限のものとする。
@ExceptionHandlerとは
@Controllerもしくは@ControllerAdviceが」付与されているクラスのメソッドに付与できるアノテーションで、コントローラーで発生したエラーをハンドリングできるもの。これにより、エラーが発生した時の処理を集約することができる。
例えば、"error"という文字列がリクエストで渡ってきたらExceptionをスローするコントローラーがあるとする。
package com.example.demo.Controller;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class SampleController {
@GetMapping("/api/sample")
public ResponseEntity<String> sample(@RequestParam String field) throws Exception {
if ("error".equals(field)) {
throw new Exception("エラーが発生しました");
}
return ResponseEntity
.status(HttpStatus.OK)
.body("正常に終了しました");
}
}
この時、以下のGlobalExceptionHandlerのように@ControllerAdviceと@ExceptionHandler({ Exception.class })を使用したクラスを用意しておけば上記でExceptionがスローされた時にhandleExceptionメソッドの処理が実行される。
package com.example.demo.handler;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.AuthenticationException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler({ Exception.class })
public ResponseEntity<String> handleException(Exception ex) {
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ex.getMessage());
}
}
試しに下記リクエストを投げてみる(-u user:passwordは後述で記載するSpring Securityの認証で使用する情報)
curl -i -X GET -u user:password 'http://localhost:8080/api/sample?field=error'
handleExceptionメソッドではExceptionで設定したエラーメッセージをレスポンスに返すようにしているが、ちゃんとレスポンスとして返ってきていることを確認できた。
HTTP/1.1 500
X-Content-Type-Options: nosniff
X-XSS-Protection: 0
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Type: text/plain;charset=UTF-8
Content-Length: 30
Date: Sat, 13 Dec 2025 08:49:36 GMT
Connection: close
エラーが発生しました
通常のFilterのエラーハンドリング
コントローラー内で起きたエラーは先ほどのようにハンドリング可能だが、Spring Securityなどで使用するFilter内の処理で発生したエラーは@ExceptionHandlerではハンドリングできない。これは先ほどのハンドリング処理がSpringMVCの仕組み上で行われるものであるからで、Filter処理はSpringMVCが動作する前に実行されるため、ハンドリングができないものとなっている。
通常Filter処理でエラーが発生したらフィルター内でエラーレスポンスを作成してクライアントに返すことになる。
ただ、エラー処理を1箇所に集約して管理しやすいようにしたいと考えたため、どうにか@ExceptionHandlerでハンドリングできないか調べてみた。
HandlerExceptionResolverを使用してFilterのエラーを@ExceptionHandlerでハンドリングする
結論、HandlerExceptionResolverを利用して@ExceptionHandlerでハンドリングすることができた。
HandlerExceptionResolverは本来はDispatcherServletの管理下にあるコントローラー内で例外を受け取った際に、「どの例外ハンドラーを使って、どのようなレスポンスを生成するか」を決定するための仕組みを提供するものであるが、フィルター内で使用することで、意図的に@ExceptionHandlerにハンドリング処理を委譲することができる。
以下はSpringSecurityを使用した認証の実装。
簡易的な実装なのでメモリ内に認証情報を持ち、リクエストで送られてきた情報を検証することで認証を行うようにしている。
authenticationEntryPointは認証が失敗した際のハンドリング処理を設定できるため、ここでハンドリング処理の委譲を行う。
package com.example.demo.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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 com.example.demo.filter.CustomAuthenticationEntryPoint;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
CustomAuthenticationEntryPoint authenticationEntryPoint() {
return new CustomAuthenticationEntryPoint();
};
/**
* メモリ内ユーザー認証の設定
*/
@Bean
UserDetailsService userDetailsService(PasswordEncoder passwordEncoder) {
// ユーザー 'user' の定義
UserDetails user = User.builder()
.username("user")
// パスワードをエンコードして格納
.password(passwordEncoder.encode("password"))
.roles("USER") // ロールを設定
.build();
// メモリ内認証マネージャーを構築
return new InMemoryUserDetailsManager(user);
}
/**
* HTTPセキュリティフィルタチェーンの設定
*/
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.anyRequest().authenticated()
)
.httpBasic(httpBasic -> httpBasic
// 認証失敗時のエラーハンドリング処理
.authenticationEntryPoint(authenticationEntryPoint())
.init(http)
)
.csrf(csrf -> csrf.disable());
return http.build();
}
}
認証に失敗した際に実行されるFilter内のハンドリング処理
package com.example.demo.filter;
import java.io.IOException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerExceptionResolver;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Autowired
@Qualifier("handlerExceptionResolver")
private HandlerExceptionResolver resolver;
@Override
public void commence(
HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
// GlobalExceptionHandlerにハンドリング処理を委譲する
resolver.resolveException(request, response, null, authException);
}
}
resolver.resolveException(request, response, null, authException);を実行することでGlobalExceptionHandlerに処理を委譲する。この時、GlobalExceptionHandlerにresolveExceptionの第4引数で渡されている例外クラスのハンドリング処理が実装されていれば、その処理が行われる。
ということでAuthenticationExceptionのハンドリング処理を追加した。
package com.example.demo.handler;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.AuthenticationException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
@ControllerAdvice
public class GlobalExceptionHandler {
// 追加
@ExceptionHandler({ AuthenticationException.class })
public ResponseEntity<String> handleAuthenticationException(AuthenticationException ex) {
return ResponseEntity
.status(HttpStatus.UNAUTHORIZED)
.body("認証失敗ですよ");
}
@ExceptionHandler({ Exception.class })
public ResponseEntity<String> handleException(Exception ex) {
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ex.getMessage());
}
}
認証失敗のエラーなので401を返すようにした。
これで以下のリクエストを実行してみる
curl -i -X GET -u user:passwordaaaa 'http://localhost:8080/api/sample?field=error'
401とエラーメッセージが返ってきているので@ExceptionHandlerでハンドリングができていることが確認できた。
HTTP/1.1 401
X-Content-Type-Options: nosniff
X-XSS-Protection: 0
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Type: text/plain;charset=UTF-8
Content-Length: 21
Date: Sat, 13 Dec 2025 09:19:43 GMT
認証失敗ですよ%
終わりに
このやり方が好ましいのか、Filter内のハンドリングは別にすべきなのかなどは勉強不足で理解が浅いですが、同じように悩む方がいたときにこの記事が誰かのお役に立てれば幸いです。