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:
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() {}
}
}
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.