做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 HttpServletResponseobject.
上面红色字体解释了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)。当然,从代码来看更直观,如下
(.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方式处理,场景考虑的比较全面,如果不考虑中途更换应用服务器可以放心使用。