最近搭建了一个独立应用,使用相同的域名去访问的时候居然报出了"Invalid CORS request", 而且GET请求是好的,唯独POST请求报403 forbidden错误,response是, Invalid CORS request。 真是奇怪啊,明明同一个域名,为什么报跨域的错误呢?
我的结构如下 : react.js --------http------------> nginx -------proxy_pass----->spring boot tomcat server.
Request URL:http://q.huster.top/experiment/infosave?_input_charset=utf-8
Request Method:POST
Status Code:403 Forbidden
Remote Address:127.0.0.1:80
Referrer Policy:no-referrer-when-downgrade
=======响应头=============
Response Headers
Connection:keep-alive
Content-Length:20
Date:Fri, 21 Jul 2017 16:06:50 GMT
Server:nginx/1.10.0
========请求头============
Request Headers
Accept:text/javascript, text/html, application/xml, text/xml, */*
Accept-Encoding:gzip, deflate
Accept-Language:zh-CN,zh;q=0.8,en;q=0.6,zh-TW;q=0.4
Connection:keep-alive
Content-Length:35
Content-Type:application/json
Cookie:cnz=kZb0Ed1glykCAfdKeCrTiSs+;
Host:quantum.aliyun.com
Origin: http://q.huster.top
Referer: http://q.huster.top/home.html
User-Agent:Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36
X-Requested-With:XMLHttpRequest
Query String Parameters
view URL encoded
_input_charset:utf-8
Request Payload
{name: "adfadsfasd", type: "REAL"}
看来看去,没毛病啊,都是同一个域名,怎么会报跨域错误呢?
在Stack Overflow上一通搜索,也没有什么头绪,太多讲的是如何使用CORS的,有的说是使用了spring boot security,因为CSRF导致的,关闭就好了,有的说是controller上加上@CorsOriginal就好了,然而这些办法我都试过了,也没什么用途。
看来今天只能靠我自己找出答案了。
这就奇怪了,以前的项目也一直是这么干的,轻车熟路,今天咋就不行了呢? 那就对比有什么差异吧。在Java代码上完全没有差异,一度怀疑是前端发送的Header不对?对比发现,这次前端多发送了一个X-Requested-With:XMLHttpReques头,会不会是这个导致的呢? 于是去查这个东西是干嘛的,就是用来标识是不是ajax请求的,那应该不会是他的原因。再加上,现在我也没办法修改前端的react代码,只能作罢。
接着排查,既然是跨域的问题,会不会跟我的跨域的配置有关系? 这里说明下,在开发阶段,为了便于前端开发,后端服务一般会配置跨域的域名,这样前后端都可以分别开发,不用相互依赖。 例如我的配置
@Configuration
public class CORSConfiguration extends WebMvcConfigurerAdapter {
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurerAdapter() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:9000",
"http://localhost:8080",
"http://127.0.0.1:9000",
"http://127.0.0.1:8080")
.allowedMethods("POST","GET");
}
};
}
}
等上线完了,正式域名了,这东西也不用修改,既便于本地调试也能直接上线。
接下来我做了两个尝试。
一、 增加一个域名把我的q.huster.top域名加进去,结果成功。按照我的理解,
同域名same origin应该不用多此一举,虽然能解决问题,但是并没有找到问题的答案
搜索了下代码,发现Invalid CORS request是由org.springframework.web.cors.DefaultCorsProcessor报出来的,
既然跟这个类有关系,那我把跨域配置删掉,不走CORS filter不就好了,于是我做了另一个尝试。
二、删除CORSConfiguration这个类,结果又成功,看来跟spring boot的CORS filter有莫大关系。同样,我也不能这么做,
因为这会给前端的联调带来很大的麻烦。
既然找到了是因为CORS filter导致的,那么我们就来看看,到底是怎么判断的是否相同域名的。
org.springframework.web.cors.DefaultCorsProcessor 错误就是这里报出来的
/**
* Invoked when one of the CORS checks failed.
* The default implementation sets the response status to 403 and writes
* "Invalid CORS request" to the response.
*/
protected void rejectRequest(ServerHttpResponse response) throws IOException {
response.setStatusCode(HttpStatus.FORBIDDEN);
response.getBody().write("Invalid CORS request".getBytes(UTF8_CHARSET));
}
具体的是由这里check的
/**
* Handle the given request.
*/
protected boolean handleInternal(ServerHttpRequest request, ServerHttpResponse response,
CorsConfiguration config, boolean preFlightRequest) throws IOException {
String requestOrigin = request.getHeaders().getOrigin();
String allowOrigin = checkOrigin(config, requestOrigin);
HttpMethod requestMethod = getMethodToUse(request, preFlightRequest);
List allowMethods = checkMethods(config, requestMethod);
List requestHeaders = getHeadersToUse(request, preFlightRequest);
List allowHeaders = checkHeaders(config, requestHeaders);
if (allowOrigin == null || allowMethods == null || (preFlightRequest && allowHeaders == null)) {
rejectRequest(response);
return false;
}
HttpHeaders responseHeaders = response.getHeaders();
responseHeaders.setAccessControlAllowOrigin(allowOrigin);
responseHeaders.add(HttpHeaders.VARY, HttpHeaders.ORIGIN);
if (preFlightRequest) {
responseHeaders.setAccessControlAllowMethods(allowMethods);
}
if (preFlightRequest && !allowHeaders.isEmpty()) {
responseHeaders.setAccessControlAllowHeaders(allowHeaders);
}
if (!CollectionUtils.isEmpty(config.getExposedHeaders())) {
responseHeaders.setAccessControlExposeHeaders(config.getExposedHeaders());
}
if (Boolean.TRUE.equals(config.getAllowCredentials())) {
responseHeaders.setAccessControlAllowCredentials(true);
}
if (preFlightRequest && config.getMaxAge() != null) {
responseHeaders.setAccessControlMaxAge(config.getMaxAge());
}
response.flush();
return true;
}
那么什么时候会执行这个,什么时候不执行呢?原来是这里判断的。
@Override
public boolean processRequest(CorsConfiguration config, HttpServletRequest request, HttpServletResponse response)
throws IOException {
if (!CorsUtils.isCorsRequest(request)) {
return true;
}
ServletServerHttpResponse serverResponse = new ServletServerHttpResponse(response);
ServletServerHttpRequest serverRequest = new ServletServerHttpRequest(request);
if (WebUtils.isSameOrigin(serverRequest)) {
logger.debug("Skip CORS processing, request is a same-origin one");
return true;
}
if (responseHasCors(serverResponse)) {
logger.debug("Skip CORS processing, response already contains \"Access-Control-Allow-Origin\" header");
return true;
}
boolean preFlightRequest = CorsUtils.isPreFlightRequest(request);
if (config == null) {
if (preFlightRequest) {
rejectRequest(serverResponse);
return false;
}
else {
return true;
}
}
return handleInternal(serverRequest, serverResponse, config, preFlightRequest);
}
接着看下调用关系, 关键代码在这里
/**
* Check if the request is a same-origin one, based on {@code Origin}, {@code Host},
* {@code Forwarded} and {@code X-Forwarded-Host} headers.
* @return {@code true} if the request is a same-origin one, {@code false} in case
* of cross-origin request.
* @since 4.2
*/
public static boolean isSameOrigin(HttpRequest request) {
String origin = request.getHeaders().getOrigin();
if (origin == null) {
return true;
}
UriComponents actualUrl = UriComponentsBuilder.fromHttpRequest(request).build();
UriComponents originUrl = UriComponentsBuilder.fromOriginHeader(origin).build();
return (actualUrl.getHost().equals(originUrl.getHost()) && getPort(actualUrl) == getPort(originUrl));
}
通过调试发现, actulurl是 127.0.0.1 而originUrl是q.huster.top,二者不相等,终于找到了问题所在。那么为什么成了127.0.0.1呢。我马上想到了nginx的配置, nginx.conf如下,通过搜索我知道了是因为host没有设置,导致host信息丢失。
http {
include mime.types;
default_type application/octet-stream;
#log_format main '$remote_addr - $remote_user [$time_local] "$request" '
#'$status $body_bytes_sent "$http_referer" '
#'"$http_user_agent" "$http_x_forwarded_for"';
#access_log logs/access.log main;
sendfile on;
#tcp_nopush on;
#keepalive_timeout 0;
keepalive_timeout 65;
#gzip on;
server {
listen 80;
server_name quantum.aliyun.com;
#charset koi8-r;
#access_log logs/host.access.log main;
location / {
root html;
index index.html index.htm;
proxy_pass http://127.0.0.1:8080;
}
于是增加两行, 问题解决。以前的nginx都不是自己装的,而是用的集团的包,这些都自动配置好了,所以一直没有遇到问题,直到今天装了官方的nginx才出现了这个问题。真相终于大白,可以好好睡觉了。
proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
如果不是这问题,还可以参考另外一篇 :http://huster.top/htmls/573.html