做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.originscors.allowed.methodscors.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中解决跨域问题的方法。通过配置Access-Control-Allow-Origin响应头,Tomcat提供了CORS过滤器来处理跨域请求,包括简单请求、预检请求和非CORS请求。文章还详细介绍了处理不同请求类型的代码逻辑,并讨论了自定义实现与使用Tomcat内置过滤器的优缺点。


被折叠的 条评论
为什么被折叠?



