本文详解JavaScript跨域访问中服务端拦截器的配置方法。重点解析CORS机制,区分简单请求与非简单请求(含OPTIONS预检)。通过Java Filter示例,展示如何设置Access-Control-Allow-Origin及withCredentials处理Cookie共享,并验证跨域请求在服务端始终完整执行的特性。
跨域后台拦截核心结论
本文主要介绍JavaScript跨域访问中,服务端如何通过后台设置进行拦截与处理。内容涉及跨域原理、配置方法及实际验证,供开发者参考学习。
在子域名相互访问的场景中,通常涉及跨域问题。
核心结论如下:
第一,服务端必须配置允许跨域的相关响应头。
第二,若客户端需携带Cookie,必须设置 withCredentials 属性。
第三,无论服务端是否允许跨域,该HTTP请求在服务端都会完整执行。
第四,对于非简单请求触发的 OPTIONS 预检请求,服务端需返回空响应或明确支持,否则若后续 requestMapping 不支持该方法,将导致错误。
环境搭建与Host配置
环境搭建
需求
首先需要构建两个运行环境:一个是提供API接口的Server A,另一个是需要跨域访问该API的Server B。
Server A提供了一个API接口。完整的请求URL如下:
https://local.corstest.com.net:8443/contentmain/getDepositsRoomAndRatePlanInfo.json?htid=759&_=1490855801818
Server B包含一个页面:
http://cros.corstest.com.net:3001/test.html
该页面需要请求Server A的API接口。
但由于浏览器的跨域保护机制,直接请求会失败:
No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'xxxxx' is therefore not allowed access.
修改Host配置
首先,在本地配置两个指向 127.0.0.1 的Host记录,以便进行跨域测试:
127.0.0.1 local.corstest.com.net
127.0.0.1 cros.corstest.com.net
启动项目A,以提供API服务。
对于项目B,测试跨域只需编写一个HTML静态页面。因此创建一个 test.html,并通过工具发布:
browser-sync
安装
npm install -g browser-sync
本地启动一个test.html
browser-sync start --server --files "*.html" --host "cros.corstest.com.net" --port 3001
CORS机制与简单/非简单请求
关于跨域CORS
根据ruanyifeng的文章,浏览器将CORS请求分为两类:简单请求(simple request)和非简单请求(not-so-simple request)。
同时满足以下两个条件的即为简单跨域:
1) 请求方法是以下三种方法之一:
- HEAD
- GET
- POST
2)HTTP的头信息不超出以下几种字段:
- Accept
- Accept-Language
- Content-Language
- Last-Event-ID
- Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain
其他情况属于非简单请求,这类请求对服务器有特殊要求,例如请求方法是 PUT 或 DELETE,或者 Content-Type 字段的类型是 application/json。非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为“预检”请求(preflight),即 OPTIONS 请求。
跨域关键与Cookie处理验证
关键点
跨域的关键在于浏览器获得服务器的认可,而服务器的认可体现在响应头中的 Access-Control-Allow-Origin 。浏览器通过比较服务端返回的response中是否包含这个字段,以及该字段内容是否为当前网址,来确定是否允许跨域。也就是说,绕过浏览器是可以不用受跨域限制的。
这里有一个常被忽略的问题。
第一点,关于Cookie的传递。浏览器设置 withCredentials 为 true 时,会将Cookie发送给服务端。而服务端设置 Access-Control-Allow-Credentials 为 true 时接收Cookie,为 false 时不接收。关键在于,当请求到达Filter时才会决定是否设置response头,此时Cookie已经存在于request中。(待验证)
验证结果显示:服务端确实已经接受了Cookie,即使设置为 false,服务端仍旧接收Cookie。而客户端也仍旧可以发送Cookie。
第二点,在简单跨域中,浏览器的请求直接发送给服务器,服务器返回是否支持跨域(即是否在header中添加origin)。那么,简单跨域究竟请求了服务端几次?如果是1次,当服务端不支持跨域(未设置allow)时,是否还会继续执行请求并返回结果放入response?即不论跨域是否允许,服务器是否都会执行该request对应的计算。因为所有header设置都是告知浏览器的,与服务端限制无关。(待验证)
验证结果显示:即使服务端没有设置允许跨域,当客户端请求过来时,服务端仍旧完整执行了请求并返回结果,只是客户端没有接收。
服务端Filter配置与测试
服务端需要做点工作
针对上述两种跨域情况,Server A需要编写一个Filter。
<filter>
<filter-name>cors</filter-name>
<filter-class>com.test.filter.CorsFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>cors</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</filter>
Filter配置:
public class CorsFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
URL requestURL = new URL(request.getRequestURL().toString());
String hostName = requestURL.getHost();
String origin = request.getHeader("Origin");
int index = hostName.indexOf(".");
if(index > -1) {
String domainHost = hostName.substring(index, hostName.length());
if(!StringUtils.isEmpty(origin) && origin.contains(domainHost)) {
response.addHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
response.addHeader("Access-Control-Allow-Origin", origin);
response.addHeader("Access-Control-Allow-Credentials", "true");
response.setHeader("Access-Control-Max-Age", "3600");
response.addHeader("Access-Control-Allow-Headers", "Content-Type, Cookie, " +
"Accept-Encoding, User-Agent, " +
"Host, Referer, " +
"X-Requested-With, Accept, " +
"Accept-Language, Cache-Control, Connection");
if (request.getHeader("Access-Control-Request-Method") != null && "OPTIONS".equals(request.getMethod())) {
// CORS "pre-flight" request
response.setStatus(200);
return;
}
}
}
filterChain.doFilter(request, response);
}
}
上述Filter旨在允许同一domain下不同子域名跨域访问,而其他domain则不可以。因为我们需要共享Cookie,所以设置 Access-Control-Allow-Credentials 为 true 。如果设置为 false 则不接受Cookie。
客户端,即Server B如果想要发送Cookie,则需要设置 withCredentials 为 true 。
//原生
var xhr = new XMLHttpRequest();
xhr.withCredentials = true;
//jquery
$.ajax({
...
xhrFields: {
withCredentials: true
}
...
});
注意:针对非简单跨域发送的 OPTIONS 请求,服务端A只需告知浏览器是否支持跨域即可,无需继续处理,否则若到达指定的requestMapping发现不支持该方法,将导致错误,因此直接返回即可。
下面针对简单跨域和非简单跨域进行测试:
<!DOCTYPE html>
<html lang="en">
<meta charset="UTF-8">
<title>test</title>
<script src="jquery-1.11.3.js"></script>
</head>
<body>
<input type="button" value="GET_Default" onclick="testGetDefault()">
<input type="button" value="GET_JSON" onclick="testGetJSON()">
<input type="button" value="POST_Default" onclick="testPostDefault()">
<input type="button" value="POST_JSON" onclick="testPostJson()">
<input type="button" value="PUT" onclick="testPUT()">
<script>
var getUrl = "https://local.corstest.com.net:8443/contentmain/getDepositsRoomAndRatePlanInfo.json?htid=759";
var postUrl = "https://local.corstest.com.net:8443/contentmain/saveReservationDeposits.json?htid=759";
function testGetDefault(){
sendAjax("GET",getUrl, "json", "application/x-www-form-urlencoded");
}
function testGetJSON(){
sendAjax("GET",getUrl, "json", "application/json; charset=utf-8");
}
function testPostDefault(){
sendAjax("POST",postUrl, "json", "application/x-www-form-urlencoded");
}
function testPostJson(){
sendAjax("POST",postUrl, "json", "application/json; charset=utf-8");
}
function testPUT(){
sendAjax("PUT",postUrl, "json", "application/json; charset=utf-8");
}
function sendAjax(type, url, dataType, contentType){
$.ajax( {
type: type,
url: url,
xhrFields: {
withCredentials: true
},
dataType : dataType, // accept type
contentType: contentType, //request type, default is application/x-www-form-urlencoded
success: function(result){
console.log(result);
},
error: function (xhr) {
console.log(xhr);
}
});
}
</script>
</body>
</html>
测试结果:
GET default:
只发送一个正常的GET请求。
GET json:
先发送一个OPTIONS预检请求如下:
General:
Request URL:https://local.corstest.com.net:8443/contentmain/getDepositsRoomAndRatePlanInfo.json?htid=759
Request Method:OPTIONS
Status Code:200 OK
Remote Address:127.0.0.1:8443
Response Headers:
Access-Control-Allow-Credentials:true
Access-Control-Allow-Headers:Content-Type, Cookie, Accept-Encoding, User-Agent, Host, Referer, X-Requested-With, Accept, Accept-Language, Cache-Control, Connection
Access-Control-Allow-Methods:GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Origin:http://cros.corstest.com.net:3001
Content-Length:0
Date:Thu, 30 Mar 2017 12:47:44 GMT
Server:Apache-Coyote/1.1
Request Headers:
Accept:*/*
Accept-Encoding:gzip, deflate, sdch, br
Accept-Language:zh-CN,zh;q=0.8
Access-Control-Request-Headers:content-type
Access-Control-Request-Method:GET
Connection:keep-alive
Host:local.corstest.com.net:8443
Origin:http://cros.corstest.com.net:3001
Referer:http://cros.corstest.com.net:3001/test.html
User-Agent:Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36
然后再发送正常的GET请求。
post default:
正常发送请求。
post json:先发送一个OPTIONS预检请求。然后再发送正常的请求。
其他情况同理,总之,非简单跨域会多发一次OPTIONS请求来确认是否支持跨域,此时服务端一定要返回支持跨域,并且直接返回即可。
总结
以上就是这篇文章的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,如果有疑问大家可以留言交流,谢谢大家对本站的支持。