做Web应用开发的,一定都遇到过或者至少听说过JS跨域这个问题。今天我们来看看这个问题的产生原因,以及在Tomcat中的解决方式。


说到跨域时,首先需要了解下浏览器的同源策略(Same orgin policy)。


那到底哪种情况下算同源,哪些情况下算跨域呢?

以下面这个URL

http://www.example.com/dir/page.html

这个URL为例,是否同源如下图所示

Tomcat与跨域问题_java

(上图来自维基百科)


而在跨域请求的时候,打开浏览器的开发者工具,会看到这行错误信息

No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://www.xxx.xxx' is therefore not allowed access.


也就是说如果要实现跨域请求,是需要服务器端明确指定的。例如我们自己开发的Servlet,要支持这种跨域请求,需要在响应头中增加如下配置

response.setHeader("Access-Control-Allow-Origin","*");


当然,真实的线上应用把星号改成对应要允许的域名即可。


而为了处理跨域的需求,Tomcat其实也包含一个特定的Filter:

org.apache.catalina.filters.CorsFilter


由于跨域资源共享英文称之为(Cross-Origin Resource Sharing),简称是CORS,在这个Filter中,我们可以定义一系列的初始参数initParam

  • cors.allowed.origins
  • cors.allowed.methods
  • cors.allowed.headers
  • ...


这一系列配置可以实现我们自己代码需要的全部功能,但更集中,方便使用。例如第一个参数可以配置允许的域有哪些,默认为星,可以指定多个域名,逗号分隔


第二个参数可以指定哪些请求方法允许使用,例如GET/POST/PUT。。。


官方文档对其功能描述如下:

This filter is an implementation of W3C's CORS (Cross-Origin Resource Sharing) specification, which is a mechanism that enables cross-origin requests.


The filter works by adding required Access-Control-* headers to HttpServletResponseobject.


上面红色字体解释了CORS的核心实现。


对于跨域的请求,在响应头中会有明显的标识


例如,我们在请求baidu首页的时候,打开开发者工具,你会发现请求的资源中,对于CSS的请求,会涉及到跨域

Tomcat与跨域问题_java_02

在响应头中会增加access-control-allow-origin标识。


接下来,我们来看Tomcat自带的Filter是如何处理的请求

 public void doFilter(final ServletRequest servletRequest,

            final ServletResponse servletResponse, final FilterChain filterChain)

            {


        // Safe to downcast at this point.

        HttpServletRequest request = (HttpServletRequest) servletRequest;

        HttpServletResponse response = (HttpServletResponse) servletResponse;


        // Determines the CORS request type.

        CorsFilter.CORSRequestType requestType = checkRequestType(request);


        // Adds CORS specific attributes to request.

        if (decorateRequest) {

            CorsFilter.decorateCORSProperties(request, requestType);

        }

        switch (requestType) {

        case SIMPLE:

            // Handles a Simple CORS request.

            this.handleSimpleCORS(request, response, filterChain);

            break;

        case ACTUAL:

            // Handles an Actual CORS request.

            this.handleSimpleCORS(request, response, filterChain);

            break;

        case PRE_FLIGHT:

            // Handles a Pre-flight CORS request.

            this.handlePreflightCORS(request, response, filterChain);

            break;

        case NOT_CORS:

            // Handles a Normal request that is not a cross-origin request.

            this.handleNonCORS(request, response, filterChain);

            break;

        default:

            // Handles a CORS request that violates specification.

            this.handleInvalidCORS(request, response, filterChain);

            break;

        }

    }


从上面的代码我们看到,对于请求的处理,还会根据请求的type来确认要使用哪种处理方式。


官方文档对于这几种type有简短的解释


  • SIMPLE: A request which is not preceded by a pre-flight request.


  • ACTUAL: A request which is preceded by a pre-flight request.


  • PRE_FLIGHT: A pre-flight request.


  • NOT_CORS: A normal same-origin request.


  • INVALID_CORS: A cross-origin request, which is invalid.


更详细的解释可以参考W3C的规范说明(https://www.w3.org/TR/cors)。当然,从代码来看更直观,如下

(.equals(method)) {
    String accessControlRequestMethodHeader =
            request.getHeader(
                    );
    (accessControlRequestMethodHeader != &&
            !accessControlRequestMethodHeader.isEmpty()) {
        requestType = CORSRequestType.;
    } (accessControlRequestMethodHeader != &&
            accessControlRequestMethodHeader.isEmpty()) {
        requestType = CORSRequestType.;
    } {
        requestType = CORSRequestType.;
    }
} (.equals(method) || .equals(method)) {
    requestType = CORSRequestType.;
} (.equals(method)) {
    String mediaType = getMediaType(request.getContentType());
    (mediaType != ) {
        (.contains(mediaType)) {
            requestType = CORSRequestType.;
        } {
            requestType = CORSRequestType.;
        }
    }


对于OPTION方式的请求会进行特殊判断,而如果只是GET请求或者普通的POST请求,都按SIMPLE来处理。

我们来看对SIMPLE类型的请求,是如何处理的。

protected void handleSimpleCORS(final HttpServletRequest request,

            final HttpServletResponse response, final FilterChain filterChain)

            throws IOException, ServletException {


        CorsFilter.CORSRequestType requestType = checkRequestType(request);

        if (!(requestType == CorsFilter.CORSRequestType.SIMPLE ||

                requestType == CorsFilter.CORSRequestType.ACTUAL)) {

            throw new IllegalArgumentException(

                    sm.getString("corsFilter.wrongType2",

                            CorsFilter.CORSRequestType.SIMPLE,

                            CorsFilter.CORSRequestType.ACTUAL));

        }

//获取源请求

        final String origin = request

                .getHeader(CorsFilter.REQUEST_HEADER_ORIGIN);

        final String method = request.getMethod();


        // Section 6.1.2

        if (!isOriginAllowed(origin)) {

            handleInvalidCORS(request, response, filterChain);

            return;

        }


        if (!allowedHttpMethods.contains(method)) {

            handleInvalidCORS(request, response, filterChain);

            return;

        }


        // Section 6.1.3

        // Add a single Access-Control-Allow-Origin header.

        if (anyOriginAllowed && !supportsCredentials) {

            // If resource doesn't support credentials and if any origin is

            // allowed

            // to make CORS request, return header with '*'.

            response.addHeader(

                    CorsFilter.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN,

                    "*");

        } else {

            response.addHeader(

                    CorsFilter.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN,

                    origin);

        }


    // Forward the request down the filter chain.

        filterChain.doFilter(request, response);

    }



我们看到,基本上是先提取请求头中的origin,根据是否允许全部请求来配置响应头。后面的参数解析也基本是这样的。


总结下,服务端对跨域的支持,我们可以自己实现,优点是不依赖Tomcat的组件,可移植性好,但可能不如Tomcat处理的场景全面或需要开发代码。而使用Tomcat自带的Filter方式处理,场景考虑的比较全面,如果不考虑中途更换应用服务器可以放心使用。