微信号:tomcat0000

介绍:由从事应用服务器核心研发的工程师维护.文章深入Tomcat源码,分析应用服务器的实现细节,工作原理.以及与之相关的技术,使用技巧,工作实战等.起于Tomcat,但不止于此.同时会分享JVM、并发等,内容多为原创...

Tomcat与跨域问题

2016-04-05 20:07 侯树成


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


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


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

以下面这个URL

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

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


(上图来自维基百科)


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

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 HttpServletResponse object.


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


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


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


在响应头中会增加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)。当然,从代码来看更直观,如下

if ("OPTIONS".equals(method)) {
String accessControlRequestMethodHeader =
request.getHeader(
REQUEST_HEADER_ACCESS_CONTROL_REQUEST_METHOD);
if (accessControlRequestMethodHeader != null &&
!accessControlRequestMethodHeader.isEmpty()) {
requestType = CORSRequestType.PRE_FLIGHT;
} else if (accessControlRequestMethodHeader != null &&
accessControlRequestMethodHeader.isEmpty()) {
requestType = CORSRequestType.INVALID_CORS;
} else {
requestType = CORSRequestType.ACTUAL;
}
} else if ("GET".equals(method) || "HEAD".equals(method)) {
requestType = CORSRequestType.SIMPLE;
} else if ("POST".equals(method)) {
String mediaType = getMediaType(request.getContentType());
if (mediaType != null) {
if (SIMPLE_HTTP_REQUEST_CONTENT_TYPE_VALUES
               .contains(mediaType)) {
requestType = CORSRequestType.SIMPLE;
} else {
requestType = CORSRequestType.ACTUAL;
}
}


对于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方式处理,场景考虑的比较全面,如果不考虑中途更换应用服务器可以放心使用。







Tomcat那些事儿

本公众号由从事应用服务器核心研发的工程师维护。文章深入Tomcat源码,分析应用服务器的实现细节,工作原理及与之相关的技术,使用技巧,工作实战等。起于Tomcat但不止于此。同时会分享并发、JVM等,内容多为原创,欢迎关注。


扫描或长按下方二维码,即可关注!




 
Tomcat那些事儿 更多文章 谁是Tomcat? 干嘛的? Tomcat 与Apache/Nginx有啥区别? 透过Tomcat配置认识其内部组件 如何加入Tomcat邮件组和开发者对话? 你可能不知道的几个java小工具
猜您喜欢 RxJava源码分析(一):Observable的几种创建方式 K-近 邻 算 法 概 述 (KNN) 浅谈HTTP中Get与Post的区别 PHP语言基础简单整理 互联网公司的加班文化