【问题标题】:How to set a response body before sending it to client如何在将响应主体发送给客户端之前设置响应主体
【发布时间】:2022-11-23 20:48:28
【问题描述】:

我们正在开发一个 Spring Boot 应用程序。控制器层的任何未知错误都由全局异常处理程序类处理,并在那里构造响应。

但是,我看到在 Spring 身份验证过滤器进行身份验证的情况下,我看到 Spring 有时会在不记录或抛出任何错误的情况下返回。

并且错误消息由 Spring 在 WWW-Authenticate 标头中提供。

现在,在这种情况下,如果任何应用程序没有处理这种情况,我只想修改响应主体,我想在响应主体中向用户传递一条解释错误消息的 JSON 消息,这样用户就不必查看标头.

Spring OncePerRequestFilter 有没有办法只修改响应体?我没有看到任何方法可以让我简单地修改身体。

【问题讨论】:

    标签: java spring spring-boot filter


    【解决方案1】:

    您可以定义一个 AuthenticationEntryPoint 并使用给定的 HttpServletResponse 根据需要编写您的响应正文。

    这是我返回翻译后的字符串作为响应正文的示例:

    import lombok.RequiredArgsConstructor;
    import org.springframework.context.support.MessageSourceAccessor;
    import org.springframework.security.core.AuthenticationException;
    import org.springframework.stereotype.Component;
    
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    
    @Component
    @RequiredArgsConstructor
    public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
    
        private final MessageSourceAccessor messages;
    
        /**
         * This is invoked when a user tries to access a secured REST resource without supplying valid credentials.
         * A 401 Unauthorized HTTP Status code will be returned as there is no login page to redirect to.
         */
        @Override
        public void commence(final HttpServletRequest request, 
                             final HttpServletResponse response,
                             final AuthenticationException authException) throws IOException {
            response.sendError(HttpServletResponse.SC_UNAUTHORIZED, messages.getMessage("error.unauthorized"));
        }
    }
    

    然后,您需要在 Spring Security 配置中注册您的 AuthenticationEntryPoint

    旧方法:

    @Configuration
    @EnableWebSecurity
    @RequiredArgsConstructor
    public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
    
        private final CustomAuthenticationEntryPoint authenticationEntryPoint;
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
              // all your other security config
              .exceptionHandling().authenticationEntryPoint(authenticationEntryPoint);
        }
    

    新的方法:

    @Configuration
    @EnableWebSecurity
    @RequiredArgsConstructor
    public class WebSecurityConfiguration {
    
        private final CustomAuthenticationEntryPoint authenticationEntryPoint;
    
        @Bean
        SecurityFilterChain testSecurityFilterChain(HttpSecurity http) throws Exception {
            return http
              // all your other security config
              .exceptionHandling().authenticationEntryPoint(authenticationEntryPoint);
        }
    }
    

    根据您的身份验证机制,Spring 提供匹配的 AuthenticationEntryPoint 实现,例如对于 OAuth,它可能是BearerTokenAuthenticationEntryPoint。如果需要,检查您当前的 AuthenticationEntryPoint 实现并复制一些逻辑到您的实现中可能很有用。

    【讨论】:

      【解决方案2】:

      Spring Security 的过滤器链在请求到达控制器之前被调用,因此过滤器链中的错误不由开箱即用的@ControllerAdvice/@ExceptionHandler 处理是正常的。

      spring-security arquitecture的一点评论

      这里可能发生两种异常:

      1. AccessDeniedException(参见AccessDeniedHandler
      2. AuthenticationException(或未经身份验证的用户)

        处理 1 应该非常简单implementing and registering an AccessDeniedHandler impl

        要处理 2,您应该实现自定义 AuthenticationEntryPoint。当用户未通过身份验证或发生 AuthenticationException 时调用此组件。

        我会给你一个关于实现的baeldung post的链接。寻找委托方法(第 4 点),因为它允许对响应进行更清晰的序列化(使用 @ExceptionHandler)。

      【讨论】:

        【解决方案3】:

        精确、应用和测试Times answer(+1)

        您可以定义一个 AuthenticationEntryPoint 并使用给定的 HttpServletResponse 根据需要编写您的响应主体。

        像这样扩展(例如)BasicAuthenticationEntryPoint(没有多少配置发送这个“WWW-Authenticated”标头):

        @Bean
        public AuthenticationEntryPoint accessDeniedHandler() {
          BasicAuthenticationEntryPoint result = new BasicAuthenticationEntryPoint() {
            // inline:
            @Override
            public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
              response.addHeader( // identic/similar to super method
                  "WWW-Authenticate", String.format("Basic realm="%s"", getRealmName())
              );
              // subtle difference:
              response.setStatus(HttpStatus.UNAUTHORIZED.value() /*, no message! */);
              // "print" custom to "response":
              response.getWriter().format(
                  "{"error":{"message":"%s"}}", authException.getMessage()
              );
            }
          };
          // basic specific/default:
          result.setRealmName("Realm");
          return result;
        }
        

        这些测试通过:

        package com.example.security.custom.entrypoint;
        
        import static org.hamcrest.Matchers.not;
        import static org.hamcrest.Matchers.empty;
        import org.junit.jupiter.api.Test;
        import org.springframework.beans.factory.annotation.Autowired;
        import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
        import org.springframework.boot.test.context.SpringBootTest;
        import org.springframework.test.web.servlet.MockMvc;
        import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.*;
        import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.unauthenticated;
        import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
        import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
        import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
        
        @AutoConfigureMockMvc
        @SpringBootTest(properties = {"spring.security.user.password=!test2me"})
        class SecurityCustomEntrypointApplicationTests {
        
          @Autowired
          private MockMvc mvc;
        
          @Test
          public void testUnathorized() throws Exception {
            mvc
                .perform(get("/secured").with(httpBasic("unknown", "wrong")))
                .andDo(print())
                .andExpect(unauthenticated());
          }
        
          @Test
          void testOk() throws Exception {
            mvc
                .perform(get("/secured").with(httpBasic("user", "!test2me")))
                .andDo(print())
                .andExpectAll(
                    status().isOk(),
                    content().string("Hello")
                );
          }
        
          @Test
          void testAccessDenied() throws Exception {
            mvc
                .perform(get("/secured"))
                .andDo(print())
                .andExpectAll(
                    status().isUnauthorized(),
                    header().exists("WWW-Authenticate"),
                    jsonPath("$.error.message", not(empty()))
                );
          }
        }
        

        在这个(完整的)应用程序上:

        package com.example.security.custom.entrypoint;
        
        import java.io.IOException;
        import javax.servlet.http.HttpServletRequest;
        import javax.servlet.http.HttpServletResponse;
        import org.springframework.boot.SpringApplication;
        import org.springframework.boot.autoconfigure.SpringBootApplication;
        import org.springframework.context.annotation.Bean;
        import org.springframework.context.annotation.Configuration;
        import org.springframework.http.HttpStatus;
        import static org.springframework.security.config.Customizer.withDefaults;
        import org.springframework.security.config.annotation.web.builders.HttpSecurity;
        import org.springframework.security.core.AuthenticationException;
        import org.springframework.security.web.AuthenticationEntryPoint;
        import org.springframework.security.web.SecurityFilterChain;
        import org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint;
        import org.springframework.stereotype.Controller;
        import org.springframework.web.bind.annotation.GetMapping;
        import org.springframework.web.bind.annotation.ResponseBody;
        
        @SpringBootApplication
        public class SecurityCustomEntrypointApplication {
        
          public static void main(String[] args) {
            SpringApplication.run(SecurityCustomEntrypointApplication.class, args);
          }
        
          @Controller
          static class SecuredController {
        
            @GetMapping("secured")
            @ResponseBody
            public String secured() {
              return "Hello";
            }
          }
        
          @Configuration
          static class SecurityConfig {
        
            @Bean
            public SecurityFilterChain filterChain(
                HttpSecurity http,
                AuthenticationEntryPoint authenticationEntryPoint
            ) throws Exception {
              http
                  .authorizeHttpRequests(
                      (requests) -> requests
                          .antMatchers("/secured").authenticated()
                          .anyRequest().permitAll()
                  )
                  .httpBasic(withDefaults())
                  .exceptionHandling()
                  .authenticationEntryPoint(authenticationEntryPoint) // ...
                  ;
              return http.build();
            }
        
            @Bean
            public AuthenticationEntryPoint accessDeniedHandler() {
              BasicAuthenticationEntryPoint result = new BasicAuthenticationEntryPoint() {
                @Override
                public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
                  response.addHeader(
                      "WWW-Authenticate", String.format("Basic realm="%s"", getRealmName())
                  );
                  response.setStatus(HttpStatus.UNAUTHORIZED.value());
                  response.getWriter().format(
                      "{"error":{"message":"%s"}}", authException.getMessage()
                  );
                }
              };
              result.setRealmName("Realm");
              return result;
            }
          }
        }
        

        【讨论】:

          猜你喜欢
          • 2022-11-04
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2020-02-29
          • 2019-07-05
          • 1970-01-01
          • 2014-10-01
          • 1970-01-01
          相关资源
          最近更新 更多