X

一次nginx导致的乌龙跨域事件

最近搭建了一个独立应用,使用相同的域名去访问的时候居然报出了"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

龙安_任天兵: 不忘初心,方得始终!