早期为了防止CSRF(跨域请求伪造)的攻击,浏览器引入了同源策略(SOP)来提高安全性。而所谓"同源策略",即同域名(domain或ip)、同端口、同协议的才能互相获取资源,而不能访问其他域的资源。在同源策略影响下,一个域名A的网页可以获取域名B下的脚本,css,图片等,但是不能发送Ajax请求,也不能操作Cookie、LocalStorage等数据。同源策略的存在,一方面提高了网站的安全性,但同时在面对前后端分离、模拟测试等场景时,也带来了一些麻烦,从而不得不寻求一些方法来突破限制,获取资源。
虽然现在大多数项目都是后端进行的跨域配置,但是前端还是有很多跨域处理的方法的。
1.首先介绍的就是最优最常用的方案,Nginx反向代理;
所谓反向代理服务器,它是代理服务器中的一种。客户端直接发送请求给代理服务器,然后代理服务器会根据客户端的请求,从真实的资源服务器中获取资源返回给客户端。所以反向代理就隐藏了真实的服务器。利用这种特性,我们可以通过将其他域名的资源映射成自己的域名来规避开跨域问题。
1.1)在webpack配置文件 /config/index.js 里找到 proxyTable 开启代理 changeOrigin:true,
proxyTable: {
'/api':{
target:'http://xx.xx.xx.xx:8080',
changeOrigin:true,
pathRewrite:{
'^/api':'/api'
}
}
},
1.2)nginx 的 配置文件 xx.conf 的 server {} 里加如下:
location /api/ {
# 把 /api 路径下的请求转发给真正的后端服务器
proxy_pass http://xx.xx.xx.xx:5568;
# 把host头传过去,后端服务程序将收到your.domain.name, 否则收到的是localhost:8080
proxy_set_header Host $http_host;
# 把cookie中的path部分从/api替换成/service
proxy_cookie_path /api /;
# 把cookie的path部分从localhost:8080替换成your.domain.name
proxy_cookie_domain localhost:80 http://xx.xx.xx.xx:5568;
}
1.3)重新启动一下 nginx ,nginx reload
- JS跨域
利用浏览器的一些特性进行hack处理,从而避开同源策略的限制。由于同源策略不会阻止动态脚本的插入到文档中去,所以催生出了一种很常用的跨域方式: JSONP
假设,我们源页面是在a.com,想要获取b.com的数据,我们可以动态插入来源于b.com 的脚本:
script = document.createElement('script');
script.type = 'text/javascript';
script.src = 'http://www.b.com/getdata?callback=demo';
这里,我们利用动态脚本的src属性,变相地发送了一个http://www.b.com/getdata?callback=demo的GET请求。这时候,b.com页面接受到这个请求时,如果没有JSONP,会正常返回json的数据结果,像这样:
{ msg: 'helloworld' }
而利用JSONP,服务端会接受这个callback参数,然后用这个参数值包装要返回的数据:
demo({msg: 'helloworld'});
这时候,如果a.com的页面上正好有一个demo 的函数:
function demo(data) {
console.log(data.msg);
}
当远程数据一返回的时候,随着动态脚本的执行,这个demo函数就会被执行。
到这里,你应该能明白这个技术为什么叫JSONP了吧?就是因为使用这种技术服务器会接受回调函数名作为请求参数,并将JSON数据填充进回调函数中去。
不过一般在实际开发的时候,我们一般会利用jQuery对JSONP的支持,而避免手写很多代码。从1.2版本开始,jQuery中加入了对JSONP的支持,可以使用$.getJSON方法来请求跨域数据:
//callback后面的?会由jQuery自动生成方法名
$.getJSON('http://www.b.com/getdata?callback=?', function(data) {
console.log(data.msg);
});
还有一种更加常用的方法是,利用$.ajax方法,只要指定dataType为jsonp 即可:
$.ajax({
url: 'http://www.b.com/getdata?callback=?', //不指定回调名,可省略callback参数,会由jQuery自动生成
dataType: 'jsonp',
jsonpCallback: 'demo', //可省略
success: function(data) {
console.log(data.msg);
}
});
虽然JSONP在跨域ajax请求方面有很强的能力,但是它也有一些缺陷。首先,它没有关于JSONP调用的错误处理,一旦回调函数调用失败,浏览器会以静默失败的方式处理。其次,它只支持GET请求,这是由于该技术本身的特性所决定的。因此,对于一些需要对安全性有要求的跨域请求,JSONP的使用需要谨慎一点了。
3. CORS 方法。
"跨域资源共享"(Cross-origin resource sharing)是W3C出的一个标准。兼容性方面可以支持IE8+(IE8和IE9需要使用XDomainRequest对象来支持CORS),所以现在CORS也已经成为主流的跨域解决方案。
CORS的核心思想是通过一系列新增的HTTP头信息来实现服务器和客户端之间的通信。所以,要支持CORS,服务端都需要做好相应的配置,这样,在保证安全性的同时也更方便了前端的开发。
浏览器会将CORS请求分为两类:简单请求和非简单请求:
简单请求
在CORS标准中,会根据是否触发CORS preflight(预请求)来区分简单请求和非简单请求。
简单请求需要满足以下几个条件:
1.请求方法只允许:GET,HEAD,POST
2.对于请求头字段有严格的要求,一般情况下不会超过以下几个字段:
Accept
Accept-Language
Content-Language
Content-Type
3.当发起POST请求时,只允许Content-Type为application/x-www-form-urlencoded,multipart/form-data,text/plain。
对于简单请求来说,服务器和客户端之间的通信只是进行简单的交换。如图:
简单请求(来源:MDN)
浏览器发送一个带有Orgin字段的HTTP请求头,用来表明请求来源。服务器的Access-Control-Allow-Origin响应头表明该服务器允许哪些源的访问,一旦不匹配,浏览器就会拒绝资源的访问。大部分情况,大家都喜欢将Access-Control-Allow-Origin设置为*,即任意外域都能访问该资源。但是,还是推荐做好访问控制,以保证安全性。
非简单请求
对于非简单请求,情况就稍微复杂了点。在正式发送请求数据之前,浏览器会先发送一个带有'OPTIONS'方法的请求来确保该请求对于目标站点来说是安全的,这个请求也被称为”预请求“(preflight)。
浏览器和服务器之间具体的交互过程如图所示:
非简单请求
浏览器会在预检请求中,多发送两个字段Access-Control-Request-Method和Access-Control-Request-Headers,前者用于告知服务器实际请求所用的方法,后者用于告知服务器实际请求所携带的自定义请求首部字段。然后,服务器将根据请求头的信息来判断是否允许该请求。
针对非简单请求,服务器端可以设置几个相关字段:
Access-Control-Allow-Methods, 用来限制允许的方法名,
Access-Control-Allow-Header,用来限制允许的自定义字段名
Access-Control-Allow-Credentials,用来表明服务器是否允许credentials标志为true的场景。
Access-Control-Max-Age,用来表明预检请求响应的有效时间
Access-Control-Expose-Headers,用来指定服务器端允许的首部字段集合
另外,如果是在具体的实践过程中,调试OPTIONS请求可以使用
curl -X OPTIONS http://xxx.com
来进行查看相应头信息。也可以通过chrome://net-internals/#events来获取更加详细的网络请求信息。
优化 CORS
针对非简单请求来说,由于每个请求都会发送预请求,这就导致接口数据的返回会有所延迟,时间被加长。所以,在使用CORS的过程中,可以采用一些方案来优化请求,将非简单请求转换成简单请求,从而提高请求的速度。
1. 请求缓存
可以在服务器端使用Access-Control-Max-Age来缓存预请求的结果。从而提高网站性能。但是需要注意的是,大部分浏览器不会允许缓存‘OPTIONS‘请求太长时间,如:火狐是24小时(86400s),chromium是10分钟(600s)。
2.针对GET 请求
对于GET请求,没必要使用Content-Type, 尽可能地保持GET请求是简单请求。这样就可以减少Header上所携带的字段。从安全性上考虑,所有的API调用应该尽可能使用https协议,而这样可以将一些授权认证信息(如token)直接放在url中去,而不必放在头部。
3.针对POST 请求
对于POST请求,我们可以尽量使用FormData这种原生的格式:
function sendQuery(url, postData) {
let formData = new FormData();
for(var key in postData) {
formData.append(key, postData.key);
}
return fetch(url, {
body: formData,
headers: {
'Accept': '/'
},
method: 'POST'
});
}
sendQuery('http://www.xxx.com', {msg: 'hello'}).then(function(response) {
//do something with response
});
附带凭证信息的请求
CORS预请求会将用户的身份认证凭据排除在外,包括cookie、http-authentication报头等。如果需要支持用户凭证,需要在XHR的withCredentials属性设置为true,同时Access-control-allow-origin不能设置为*。在服务器端会利用响应报头Access-Control-Allow-Credentials来声明是否支持用户凭证。
同时,利用withCredentials这个属性,也可以检测浏览器是否支持CORS。下面创建一个带有兼容性处理的cors 请求:
function createCORSRequest(method, url){
var xhr = new XMLHttpRequest();
if ("withCredentials" in xhr){
xhr.open(method, url, true);
} else if (typeof XDomainRequest != "undefined"){
xhr = new XDomainRequest();
xhr.open(method, url);
} else {
xhr = null;
}
return xhr;
}
var request = createCORSRequest("POST", "http://www.xxx.com");
if (request){
request.onload = function(){
//do something with request.responseText
};
request.send();
}
如果浏览器支持fetch,则使用它做跨域请求更加方便:
fetch('http://www.xxx.com', {
method: 'POST',
mode: 'cors',
credentials: 'include' //接受凭证
}).then(function(response) {
//do something with response
});