Spring Boot (Spring Security)で、未保護エンドポイントからResponseStatusExceptionをスローしても、指定したステータスコードで返却されない

実現したいこと

Spring Boot (Spring Security)で、以下の機能を実現するREST APIを作成しようとしています。

  1. 未保護エンドポイント /unprotect にアクセスすると、HTTPStatus 200が返ってくる
  2. 保護エンドポイント /protect にアクセスすると、Basic認証が要求される
    1. Basic認証に成功すると、HTTPStatus 200が返ってくる
    2. Basic認証に失敗すると、HTTPStatus 500と個有のレスポンスボディが返ってくる
  3. /unprotect、/protectいずれも、業務ロジックに応じて、特定のHTTPStatusを返却する

/unprotect のコントローラーから、ResponseStatusExceptionをスローしても、指定したステータスコードで返却されない問題が発生しており、こちらの問題を解消したいです。

前提

未保護エンドポイント、保護エンドポイントはURLで切り替えています。
Basic認証に失敗した場合に、個有のレスポンスボディを返却する必要があるため、authenticationEntryPointを指定しています。
実装は以下の通りです。

java

1@Configuration2@EnableWebSecurity3public class SecurityConfig {4 5 @Bean6 public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {7 http.csrf(csrf -> csrf.disable())8 .authorizeHttpRequests(a -> a 9 .requestMatchers("/unprotect").permitAll()10 .anyRequest().authenticated())11 .formLogin(f -> f.disable())12 .httpBasic(Customizer.withDefaults())13 .exceptionHandling((e) -> e.authenticationEntryPoint(new MyAuthenticationEntryPoint()));14 15 return http.build();16 }17}

java

1@Slf4j2public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {3 4 @Override5 public void commence(HttpServletRequest request, HttpServletResponse response,6 AuthenticationException authException) throws IOException, ServletException {7 8 log.info("call MyAuthenticationEntryPoint commence");9 10 authException.printStackTrace();11 12 response.getWriter().write("MyAuthenticationEntryPoint response");13 response.setStatus(500);14 }15}

コントローラーでは、クエリパラメーター "p" をつけてリクエストするとHttpStatus 200、なければHttpStatus 400を返すようにしています。
実装は以下の通りです。

java

1@RestController2@Slf4j3public class MyController {4 5 @GetMapping("/protect")6 public String protect(@RequestParam(value = "p", required = false) String p) {7 log.info("call protect");8 return hello(p);9 }10 11 @GetMapping("/unprotect")12 public String unprotect(@RequestParam(value = "p", required = false) String p) {13 log.info("call unprotect");14 return hello(p);15 }16 17 private String hello(String e) {18 if (e == null || "".equals(e)) {19 log.info("throw exception");20 throw new ResponseStatusException(HttpStatus.BAD_REQUEST);21 } else {22 log.info("return response");23 return "Hello, " + e;24 }25 }26 27}

認証を行う部分は以下のように実装しています。

java

1@Component2public class MyAuthenticationProvider implements AuthenticationProvider {3 4 @Override5 public Authentication authenticate(final Authentication authentication) {6 7 final Object principal = authentication.getPrincipal();8 if (!"myname".equals(authentication.getName())) {9 throw new BadCredentialsException("name is not myname");10 }11 12 final UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(principal,13 authentication.getCredentials(), Collections.emptyList());14 result.setDetails(authentication.getDetails());15 16 return result;17 }18 19 @Override20 public boolean supports(final Class<?> authentication) {21 return true;22 }23}

発生している問題・エラーメッセージ

未保護エンポイント /unprotectに "p"パラメーターなしでアクセスすると、求めているHTTPStatus 400ではなく、MyAuthenticationEntryPointで返却しているレスポンスが返ってきます。

$ curl -D - localhost:8080/unprotect HTTP/1.1 500 Vary: Origin Vary: Access-Control-Request-Method Vary: Access-Control-Request-Headers 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 Set-Cookie: JSESSIONID=5DF3BF5182D9CCAC62D7B8BA3BF36E1F; Path=/; HttpOnly Content-Length: 35 Date: Sun, 28 Jan 2024 08:37:20 GMT Connection: close MyAuthenticationEntryPoint response

このとき、Spring Boot側には、以下のログが出力されています。
コントローラーのメソッドを処理したあとに、MyAuthenticationEntryPointが呼ばれています。

2024-01-28T17:41:36.910+09:00 INFO 59621 --- [nio-8080-exec-3] com.example.MyController : call unprotect 2024-01-28T17:41:36.910+09:00 INFO 59621 --- [nio-8080-exec-3] com.example.MyController : throw exception 2024-01-28T17:41:36.910+09:00 WARN 59621 --- [nio-8080-exec-3] .w.s.m.a.ResponseStatusExceptionResolver : Resolved [org.springframework.web.server.ResponseStatusException: 400 BAD_REQUEST] 2024-01-28T17:41:36.911+09:00 INFO 59621 --- [nio-8080-exec-3] com.example.MyAuthenticationEntryPoint : call MyAuthenticationEntryPoint commence org.springframework.security.authentication.InsufficientAuthenticationException: Full authentication is required to access this resource at org.springframework.security.web.access.ExceptionTranslationFilter.handleAccessDeniedException(ExceptionTranslationFilter.java:199) at org.springframework.security.web.access.ExceptionTranslationFilter.handleSpringSecurityException(ExceptionTranslationFilter.java:178) at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:147) at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:120) at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) at org.springframework.security.web.authentication.AnonymousAuthenticationFilter.doFilter(AnonymousAuthenticationFilter.java:100) at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) at org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter.doFilter(SecurityContextHolderAwareRequestFilter.java:179) at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) at org.springframework.security.web.savedrequest.RequestCacheAwareFilter.doFilter(RequestCacheAwareFilter.java:63) at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:101) at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:107) at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:93) at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:101) at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:101) at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) at org.springframework.security.web.context.SecurityContextHolderFilter.doFilter(SecurityContextHolderFilter.java:82) at org.springframework.security.web.context.SecurityContextHolderFilter.doFilter(SecurityContextHolderFilter.java:69) at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:101) at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:101) at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:233) at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:191) at org.springframework.web.filter.CompositeFilter$VirtualFilterChain.doFilter(CompositeFilter.java:113) at org.springframework.web.servlet.handler.HandlerMappingIntrospector.lambda$createCacheFilter$3(HandlerMappingIntrospector.java:195) at org.springframework.web.filter.CompositeFilter$VirtualFilterChain.doFilter(CompositeFilter.java:113) at org.springframework.web.filter.CompositeFilter.doFilter(CompositeFilter.java:74) at org.springframework.security.config.annotation.web.configuration.WebMvcSecurityConfiguration$CompositeFilterChainProxy.doFilter(WebMvcSecurityConfiguration.java:225) at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:352) (省略)

例外をスローしない状態なら、想定しているHttpStatus 200が返ってきます。
そのため、/unprotect はSpring Securityで保護されていないという認識です。

$ curl -D - localhost:8080/unprotect?p=unprotect HTTP/1.1 200 Vary: Origin Vary: Access-Control-Request-Method Vary: Access-Control-Request-Headers 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: 16 Date: Sun, 28 Jan 2024 08:46:35 GMT Hello, unprotect

/unprotect のコントローラーにて、ResponseStatusExceptionをスローした際に、指定したステータスコードで返却するには、どうしたらよいのでしょうか?

補足情報(FW/ツールのバージョンなど)

  • Spring Boot 3.2.2

コメントを投稿

0 コメント