最近搭建了一个独立应用,使用相同的域名去访问的时候居然报出了"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<HttpMethod> allowMethods = checkMethods(config, requestMethod); List<String> requestHeaders = getHeadersToUse(request, preFlightRequest); List<String> 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