実現したいこと
Spring Boot (Spring Security)で、以下の機能を実現するREST APIを作成しようとしています。
- 未保護エンドポイント /unprotect にアクセスすると、HTTPStatus 200が返ってくる
- 保護エンドポイント /protect にアクセスすると、Basic認証が要求される
- Basic認証に成功すると、HTTPStatus 200が返ってくる
- Basic認証に失敗すると、HTTPStatus 500と個有のレスポンスボディが返ってくる
- /unprotect、/protectいずれも、業務ロジックに応じて、特定のHTTPStatusを返却する
/unprotect のコントローラーから、ResponseStatusExceptionをスローしても、指定したステータスコードで返却されない問題が発生しており、こちらの問題を解消したいです。
前提
未保護エンドポイント、保護エンドポイントはURLで切り替えています。
Basic認証に失敗した場合に、個有のレスポンスボディを返却する必要があるため、authenticationEntryPointを指定しています。
実装は以下の通りです。
java
1@Configuration 2@EnableWebSecurity 3public class SecurityConfig { 4 5 @Bean 6 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@Slf4j 2public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint { 3 4 @Override 5 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@RestController 2@Slf4j 3public 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@Component 2public class MyAuthenticationProvider implements AuthenticationProvider { 3 4 @Override 5 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 @Override 20 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

回答1件
あなたの回答
tips
プレビュー
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。