[英]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:如果到启用 Spring Security (v 5.6.1) 的服务端点的 Fetch POST 发送此标头:
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;过滤器链中的任何过滤器都不会处理 OPTIONS 预检请求 - 过滤器日志不会显示来自任何链过滤器的任何响应; there isn't even a server-side invocation of
org.apache.catalina.connector.RequestFacade
in response to the preflight call.甚至没有响应预检调用的
org.apache.catalina.connector.RequestFacade
的服务器端调用。 The HttpResponse the client receives will always just show 403 Unauthorized
.客户端收到的 HttpResponse 将始终只显示
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.因此,这种请求不会导致预检,然后它将通过过滤器链传播并由启用的 CorsFilter 处理,该 CorsFilter 将添加必要的标头以满足预检并返回此响应。 It's that the preflight is apparently simply rejected outright if application/json is the content-type.
如果 application/json 是内容类型,那么预检显然会被彻底拒绝。 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:自定义CorsConfig:
@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. Spring Security 仅通过过滤器链运行。 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.如果它正在使用中,
@CrossOrigin
控制器注释将无关紧要(不会应用),因为过滤器链过滤器将在请求到达控制器端点之前执行。
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:我发现让 SpringBoot (v 2.6.2) 服务处理请求的唯一方法是将标头设置为:
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.但这当然意味着任何/所有服务端点都不能
consume = application/json
,因为从指定content-type = application/json
的获取客户端调用它们都将受到 OPTIONS 预检。
This can't be the situation Spring Security envisioned;这不可能是 Spring Security 所设想的情况; Fetch-to-SpringSecurity Microservice is a very prevalent implementation.
Fetch-to-SpringSecurity 微服务是一个非常流行的实现。 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此外,如果
comsumes = text/plain
没有一个 Spring 的开箱即用 HttpMessageConverters 成功地将端点的指定@RequestBody
映射到指定的域 POJO 类型 - 尝试这样做会导致
[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?
另外,有没有办法直接向 Spring Security 开发人员提出这个问题?
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
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.