简体   繁体   中英

Spring Security Rejects Fetch OPTIONS Preflight Outright If content-type = 'application/json'

If a Fetch POST to a Spring Security (v 5.6.1) enabled service endpoint sends this header:

headers.append("Content-Type", "application/json");

the OPTIONS preflight request will not be handled by any filter in the filter chain - filter logging shows no response whatsoever from any chain filter; there isn't even a server-side invocation of org.apache.catalina.connector.RequestFacade in response to the preflight call. The HttpResponse the client receives will always just show 403 Unauthorized .

So it isn't that this kind of request causes a preflight, which would then propagate through the filter chain and be handled by an enabled CorsFilter which would add the requisite headers to satisfy the preflight and return this response. It's that the preflight is apparently simply rejected outright if application/json is the content-type. No service response to it at all.

This situation exists with this config in place:

@Configuration
@EnableWebSecurity(debug = true)
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurity extends WebSecurityConfigurerAdapter {

    private Environment environment;
    private UserService service;
    final Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
    public WebSecurity(Environment environment,
                       UserService service) {
        this.environment = environment;
        this.service = service;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.addFilterBefore(new CustomCorsFilter(), UsernamePasswordAuthenticationFilter.class);
        http.csrf().disable();
        http.authorizeRequests()
            .antMatchers("/users/**")
            .hasIpAddress(environment.getProperty("gateway.ip"));
        http.headers().frameOptions().disable();
    }
}

CustomCorsConfig:

@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class CustomCorsFilter extends CorsFilter {

    final Logger logger = LoggerFactory.getLogger(getClass());

    public CustomCorsFilter() {
        super(configurationSource());
    }
    
    private static UrlBasedCorsConfigurationSource configurationSource() {
        List<String> allHeaders = Arrays.asList("X-Auth-Token",
                                                "Content-Type",
                                                "X-Requested-With",
                                                "XMLHttpRequest",
                                                "Accept",
                                                "Key",
                                                "Authorization",
                                                "X-Authorization");
        List<String> allowedMethods = Arrays.asList("GET","POST","OPTIONS");
        List<String> allowedOrigins = Arrays.asList("http://localhost:3000", "http://localhost:8082");
        CorsConfiguration corsConfig = new CorsConfiguration();
        corsConfig.setAllowedHeaders(allHeaders);
        corsConfig.setAllowedMethods(allowedMethods);
        corsConfig.setAllowedOrigins(allowedOrigins);
        corsConfig.setExposedHeaders(allHeaders);
        corsConfig.setMaxAge(3600L);
        corsConfig.setAllowCredentials(true);
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", corsConfig);
        return source;
    }
    
    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain)
            throws ServletException, IOException {

        response.setContentType("application/json");
        response.setCharacterEncoding("UTF-8");
        response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, "http://localhost:3000");
        response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
        response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, "GET, POST, OPTIONS");
        response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, "Origin, Content-Type, Accept");
        Object[] headerNames = response.getHeaderNames().toArray();
        String names = "";
        for (Object o : headerNames) {
            String s = (String)o;
            names += s + ", ";
        }
        logger.info("\n ** CustomCorsFilter.doFilterInternal(): header names are: " + names + "\n\n");
        if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
            response.setStatus(HttpServletResponse.SC_OK);
        } else {
            filterChain.doFilter(request, response);
        }
    }

    @Override
    protected boolean shouldNotFilterAsyncDispatch() {
        return false;
    }

    @Override
    protected boolean shouldNotFilterErrorDispatch() {
        return false;
    }

}

Spring Security runs only via the filter chain. If it's in use, the @CrossOrigin controller annotation will be irrelevant (won't be applied) since the filter chain filters will execute before the request gets to the controller endpoint.

The only way I found to get the request handled by the SpringBoot (v 2.6.2) service at all was to set the header as:

headers.append("Content-Type", "text/plain");

But this of course means that any/all service endpoints can't consume = application/json , since being called from a fetch client specifying content-type = application/json they would all be subject to an OPTIONS preflight.

This can't be the situation Spring Security envisioned; Fetch-to-SpringSecurity Microservice is a very prevalent implementation. Also, if comsumes = text/plain none of Spring's out-of-box HttpMessageConverters successfully map the endpoint's specified @RequestBody to a specified domain POJO type - trying this causes

[org.springframework.web.HttpMediaTypeNotSupportedException: Content type 'text/plain;charset=UTF-8' not supported]

Please let me know what I'm missing here. Also, is there a way to pose this issue directly to Spring Security dev?

Here's the solution:

  1. Add this class:
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.access.channel.ChannelProcessingFilter;
import org.springframework.web.cors.CorsUtils;

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    private Environment environment;
    final Logger logger = LoggerFactory.getLogger(getClass());
    
    @Autowired
    public WebSecurityConfig(Environment environment) {
        this.environment = environment;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .antMatchers("/xxx/**")
            .hasIpAddress(environment.getProperty("gateway.ip"))
            .requestMatchers(CorsUtils::isCorsRequest).permitAll()
            .and().addFilterBefore(new WebSecurityCorsFilter(),
                                   ChannelProcessingFilter.class);
        http.headers().frameOptions().disable();
    }

    class WebSecurityCorsFilter implements Filter {
        
        @Override
        public void init(FilterConfig filterConfig) throws ServletException {}
        
        @Override
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) 
                throws IOException, ServletException {
            logger.info("\n\nIs this thing on?\n");
            HttpServletResponse res = (HttpServletResponse) response;
            res.setHeader("Access-Control-Allow-Origin", "http://localhost:3000");
            res.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE, PUT");
            res.setHeader("Access-Control-Max-Age", "3600");
            res.setHeader("Access-Control-Allow-Headers", "Authorization, Content-Type, Accept, x-requested-with, Cache-Control");
            chain.doFilter(request, res);
        }
        
        @Override
        public void destroy() {}

    }

}
  1. CORS support is disabled by default and is only enabled once the management.endpoints.web.cors.allowed-origins property has been set. Add these lines to application.properties to enable CORS filtering:
management.endpoints.web.cors.allowed-origins=http://localhost:3000
management.endpoints.web.cors.allowed-methods=GET,POST

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM