在JavaScript中,有一个很重要的安全性限制,被称为“Same-Origin Policy”(同源策略),由于浏览器这个同源策略,凡是发送请求的url的协议、域名、端口三者之间任意一与当前页面地址不同即为跨域。具体可以查看下表:
如果你进行了跨域请求,你在浏览器控制台就会看到以下提示:
XMLHttpRequest cannot load http://external-domain/service. No ‘Access-Control-Allow-Origin’ header is present on the requested resource. Origin ‘http://my-domain’ is therefore not allowed access.
跨域请求并非是浏览器限制了发起跨站请求,而是请求可以正常发起,到达服务器端,但是服务器返回的结果会被浏览器拦截。
特别注意两点:
第一,如果是协议和端口造成的跨域问题“前台”是无能为力的,
第二:在跨域问题上,域仅仅是通过“URL的首部”来识别而不会去尝试判断相同的ip地址对应着两个域或两个域是否在同一个ip上。
“URL的首部”是指window.location.protocol +window.location.host,也可以理解为“Domains, protocols and ports must match”。
原因就是安全问题:如果一个网页可以随意地访问另外一个网站的资源,那么就有可能在客户完全不知情的情况下出现安全问题。比如下面的操作就有安全问题:
既然有安全问题,那为什么又要跨域呢? 我们知道,在页面上有三种资源是可以与页面本身不同源的。它们是:js脚本,css样式文件,图片,像淘宝等大型网站,肯定会将这些静态资源放入cdn中,然后在页面上连接。如果非同源,还有三种行为受到限制:
(1) Cookie、LocalStorage 和 IndexDB 无法读取。
(2) DOM 无法获得。
(3) AJAX 请求不能发送。
一个公司想从子域去访问另一个子域的资源就就必须得跨域请求。
JSONP是JSON with Padding(填充式json)的简写。在js中,我们直接用XMLHttpRequest请求不同域上的数据时,是不可以的。但是,在页面上引入不同域上的js脚本文件却是可以的。
所以它的基本思想就是,网页通过添加一个元素,向服务器请求JSON数据,这种做法不受同源政策限制;服务器收到请求后,将数据放在一个指定名字的回调函数里传回来。所以jsonp是需要服务器端的页面进行相应的配合的。
JSONP属于单项跨域(一般用来获取数据),由两部分组成: 回调函数和数据。看以下代码:
或者通过动态生成元素,然后通过src属性指定一个跨域URL:
function getPrice(data){
console.log(data);
}
var script = document.createElement("script");
script.src = "example.com?callback=getPrice&bcid=47296567";
document.body.insertBefore(script, document.body.firstChild);
上面代码向服务器example.com发出请求,该请求的查询字符串有一个callback参数,用来指定回调函数的名字,这对于JSONP是必需的。callback是前后台约定的查询参数,服务器收到这个请求以后,会将数据放在回调函数的参数位置返回。
PHP
或者Python
app.get('/jsonpHandler', (req, res) => {
let callback = req.query.callback;
let obj = {
"data":{"priceType":"0","unit":"斤"},
"message":"价格获取成功!!!",
"state":"1"
};
res.writeHead(200, {"Content-Type": "text/javascript"});
res.end(callback + '(' + JSON.stringify(obj) + ')');
})
除此之外,还可以利用jQuery来实现。
jquery会自动生成一个全局函数来替换callback=?中的问号,之后获取到数据后又会自动销毁,实际上就是起一个临时代理函数的作用。$.getJSON方法会自动判断是否跨域,不跨域的话,就调用普通的ajax方法;跨域的话,则会以异步加载js文件的形式来调用jsonp的回调函数。
或者
function jsonCallback(json){
console.log(json);
}
$.ajax({
url: "http://run.plnkr.co/plunks/v8xyYN64V4nqCshgjKms/data-2.json",
dataType: "jsonp"
});
或者
function logResults(json){
console.log(json);
}
$.ajax({
url: "https://api.github.com/users/jeresig",
dataType: "jsonp",
jsonpCallback: "logResults"
});
JSONP最大特点就是简单适用,老式浏览器全部支持,不需要XMLHttpRequest或ActiveX的支持,服务器改造非常小。但是也有其缺点:
1、这种方式无法发送post请求
2、不能很好的发现错误,并进行处理,与 Ajax 对比,由于不是通过 XmlHttpRequest 进行传输,所以不能注册 success、 error 等事件监听函数。大多数框架的实现都是结合超时时间来判定。
3、它只支持跨域HTTP请求这种情况,不能解决不同域的两个页面之间如何进行JavaScript调用的问题。
WebSocket是一种通信协议,使用ws://(非加密)和wss://(加密)作为协议前缀。该协议不实行同源政策,只要服务器支持,就可以通过它进行跨源通信。
WebSocket的原理:在js创建了WebSocket之后,会有一个HTTP请求发送到浏览器以发起连接。取得服务器响应后,建立的连接会使用HTTP升级从HTTP协议交换为WebSocket协议。
var socket = new WebSockt('ws://www.baidu.com');//http->ws; https->wss
socket.send('hello WebSockt');
socket.onmessage = function(event){
var data = event.data;
}
CORS是跨源资源分享(Cross-Origin Resource Sharing)的缩写。也需要浏览器和服务器同时支持。
原理是使用"Origin:"请求头和"Access-Control-Allow-Origin"响应头来扩展HTTP。其实就是利用新的HTTP头部来进行浏览器与服务器之间的沟通。实现CORS通信的关键是服务器。
浏览器将CORS请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)。非简单请求会多发出一个method为options的请求,来询问是否允许跨源请求。
通过setRequestHeader(‘X-Request-With’, null)可以避免浏览器发送OPTIONS请求。
针对前端代码而言,变化的地方在于相对路径需改为绝对路径。
//以前的方式
var xhr = new XMLHttpRequest();
xhr.onload = function(data) {
var _data = JSON.parse(data.target.responseText)
for(key in _data) {
console.log('key: ' + key + ' value: ' + _data[key]);
}
};
xhr.open("GET", "/test", true);
xhr.setRequestHeader('Content-Type','application/x-www-form-urlencoded');
xhr.send();
//CORS方式
var xhr = new XMLHttpRequest();
xhr.onload = function(data) {
var _data = JSON.parse(data.target.responseText)
for(key in _data) {
console.log('key: ' + key + ' value: ' + _data[key]);
}
};
xhr.open("GET", "http://segmentfault.com/test", true);
xhr.setRequestHeader('Content-Type','application/x-www-form-urlencoded');
xhr.send();
针对服务器代码而言,需要设置Access-Control-Allow-Origin,显式地列出源或使用通配符来匹配所有源:
app.post('/cors', (req, res) => {
if(req.headers.origin) {
res.writeHead(200, {
"Content-Type": "text/html; charset=UTF-8",
"Access-Control-Allow-Origin": 'http://127.0.0.1:8888'
});
let people = {
type: 'cors',
name: 'weapon-x'
}
res.end(JSON.stringify(people));
}
})
优点:
不足:
详情可参考:跨域资源共享 CORS 详解、HTTP访问控制(CORS)
只有在主域相同的时候才能使用该方法。浏览器中不同域的框架之间是不能进行js的交互操作的。但是不同的框架之间(父子或同辈),是能够获取到彼此的window对象的,但是,我们也只能获取到一个几乎无用的window对象。比如,有一个页面,它的地址是 http://www.example.com/a.html , 在这个页面里面有一个iframe,它的src是 http://example.com/b.html , 很显然,这
个页面与它里面的iframe框架是不同域的,所以我们是无法通过在页面中书写js代码来获取iframe中的东西的。
这个时候,document.domain就可以派上用场了,我们只要把 http://www.example.com/a.html 和http://example.com/b.html 这两个页面的document.domain都设成相同的域名就可以了。但要注意的是,document.domain的设置是有限制的,我们只能把document.domain设置成自身或更高一级的父域,且主域必须相同。例如:
a.b.example.com 中某个文档的document.domain 可以设成a.b.example.com、b.example.com 、example.com中的任意一个,但是不可以设成 c.a.b.example.com,因为这是当前域的子域,也不可以设成baidu.com,因为主域已经不相同了。
比如在http://www.example.com/a.html 的页面里要访问 http://example.com/b.html里面的东西。
在页面 http://www.example.com/a.html 中设置document.domain:
A页面
// 相当于用一个隐藏的iframe来做代理
在页面 http://example.com/b.html 中也设置document.domain,而且这也是必须的,虽然这个文档的domain就是example.com,但是还是必须显示的设置document.domain的值:
B页面
这里有个注意点,就是在A页面中,要等iframe标签完成加载B页面之后,再取iframe对象的contentDocument,否则如果B页面没有被iframe完全加载,在A页面中通过contentDocument属性就取不到B页面中的jQuery对象。
一旦取到B页面中的jQuery对象,就可以直接发ajax请求了,这种类似“代理”方式可以解决主子域的跨域问题。
缺点:
postMessge()是HTML5新定义的通信机制。该API定义在Window对象。
window.postMessage(message, targetOrigin);
message: 将要发送的消息,可以使一切javascript参数,如字符串,数字,对象,数组等。
targetOrigin: 指定目标窗口的源。在发送消息的时候,如果目标窗口的协议、主机地址或端口这三者的任意一项不匹配targetOrigin提供的值,那么消息就不会被发送;只有三者完全匹配,消息才会被发送。这个机制用来控制消息可以发送到哪些窗口;
当源匹配时,调用postMessage()方法时,目标窗口的Window对象会触发一个message事件。message事件监听函数接收一个参数event,event对象实例,该对象有三个属性:
在进行监听事件时,应先判断origin属性,忽略来自未知源的消息。
//上的脚本:
var popup = window.open(...popup details...);
popup.postMessage("The user is 'bob' and the password is 'secret'",
"https://secure.example.net");
popup.postMessage("hello there!", "http://example.org");
function receiveMessage(event)
{
if (event.origin !== "http://example.org")
return;
// event.source is popup
// event.data is "hi there yourself! the secret response is: rheeeeet!"【见下面一段代码可知】
}
window.addEventListener("message", receiveMessage, false);
针对上面的脚本进行接受数据的操作:
/*
* popup的脚本,运行在:
*/
//当postMessage后触发的监听事件
function receiveMessage(event)
{
//先判断源
if (event.origin !== "http://example.com:8080")
return;
// event.source:window.opener
// event.data:"hello there!"
event.source.postMessage("hi there yourself! the secret response " +
"is: rheeeeet!",
event.origin);
}
window.addEventListener("message", receiveMessage, false);
缺点:ie8+才支持,而且ie8+ 详情还可参考:Window.postMessage() window对象有个name属性,该属性有个特征:即在一个窗口 (window) 的生命周期内,窗口载入的所有的页面都是共享一个 window.name 的,每个页面对 window.name 都有读写的权限,window.name 是持久存在一个窗口载入过的所有页面中的,并不会因新页面的载入而进行重置。 同样这个方法也可以应用到和iframe的交互来。 或者将里面的 about:blank 替换成某个同源页面(about:blank,javascript: 和 data: 中的内容,继承了载入他们的页面的源。) 此方法主要是应用当frame的页面跳到其他地址时,其window.name值保持不变的原理。兼容性好。照顾落后的浏览器时,可首选。 在前后端分离的项目中可以借助服务器实现跨域,具体做法是:前端向本地服务器发送请求,本地服务器代替前端再向真实服务器接口发送请求进行服务器间通信,本地服务器其实充当个「中转站」的角色,再将响应的数据返回给前端,来看下实际例子: 后端: 需要注意的是如果你代理的是https协议的请求,那么你的proxy首先需要信任该证书(尤其是自定义证书)或者忽略证书检查,否则你的请求无法成功。 原理是利用location.hash来进行传值。在url: 这是IE8、IE9提供的一种跨域解决方案,功能较弱只支持get跟post请求,而且对于协议不同的跨域是无能为力的,比如在http协议下发送https请求。 图像ping是与服务器进行简单、单向的跨域通信的一种方式,请求的数据是通过查询字符串的形式发送的,而相应可以是任意内容,但通常是像素图或204相应(No Content)。 图像ping有两个主要缺点:首先就是只能发送get请求,其次就是无法访问服务器的响应文本。 与类似的可以跨域内嵌资源的还有: (1)标签嵌入跨域脚本。语法错误信息只能在同源脚本中捕捉到。上面jsonp也用到了呢。 (2) 标签嵌入CSS。由于CSS的松散的语法规则,CSS的跨域需要一个设置正确的Content-Type消息头。不同浏览器有不同的限制: IE, Firefox, Chrome, Safari (跳至CVE-2010-0051)部分 和 Opera。 (3) 和 嵌入多媒体资源。 (4), 和 的插件。 (5)@font-face引入的字体。一些浏览器允许跨域字体( cross-origin fonts),一些需要同源字体(same-origin fonts)。 (6) 和 载入的任何资源。站点可以使用X-Frame-Options消息头来阻止这种形式的跨域交互。 这里先不做详解。 参考:浏览器同源政策及其规避方法6、window.name
基于这个思想,我们可以在某个页面设置好 window.name 的值,然后跳转到另外一个页面。在这个页面中就可以获取到我们刚刚设置的 window.name 了。不过由于安全原因,window.name 必须是string 类型的,但可以支持非常长的 name 值(2MB)。
var iframe = document.getElementById('iframe');
var data = '';
iframe.onload = function() {
iframe.onload = function(){
data = iframe.contentWindow.name;
}
iframe.src = 'about:blank';
};
这种方法与 document.domain 方法相比,放宽了域名后缀要相同的限制,可以从任意页面获取 string 类型的数据。7、服务器端Proxy代理跨域
前端:// http://127.0.0.1:8888/server
var xhr = new XMLHttpRequest();
xhr.onload = function(data) {
var _data = JSON.parse(data.target.responseText)
for(key in _data) {
console.log('key: ' + key +' value: ' + _data[key]);
}
};
xhr.open('POST','http://127.0.0.1:8888/feXhr',true); // 向本地服务器发送请求
xhr.setRequestHeader('Content-Type','application/x-www-form-urlencoded');
xhr.send("url=http://127.0.0.1:2333/beXhr"); // 以参数形式告知需要请求的后端接口
// http://127.0.0.1:8888/feXhr
app.post('/feXhr', (req, res) => {
let url = req.body.url;
superagent.get(url) //使用 superagent 向实际接口发起请求
.end((err, docs) => {
if(err) {
console.log(err);
return
}
res.end(docs.res.text); // 返回给前端
})
})
// http://127.0.0.1:2333/beXhr
app.get('/beXhr', (req, res) => {
let obj = {
type: 'superagent',
name: 'weapon-x'
};
res.writeHead(200, {"Content-Type": "text/javascript"});
res.end(JSON.stringify(obj)); //响应
})
还需要注意一点,对于同一请求浏览器通常会从缓存中读取数据,我们有时候不想从缓存中读取,所以会加一个preventCache参数,这个时候请求url变成:url?preventCache=12345567…;这本身没有什么问题,问题出在当使用某些前端框架(比如jquery)发送proxy代理请求时,请求url为proxy?url,同时设置preventCache:true,框架不能正确处理这个参数,结果发出去的请求变成proxy?url&preventCache=123456(正常应为proxy?url?preventCache=12356);后端截取后发送的请求为url&preventCache=123456,根本没有这个地址,所以你得不到正确结果。8、iframe和location.hash
http://a.com#helloword
中的‘#helloworld’就是location.hash,改变hash并不会导致页面刷新,所以可以利用hash值来进行数据传递,当然数据容量是有限的。假设域名a.com下的文件cs1.html要和cnblogs.com域名下的cs2.html传递信息,cs1.html首先自动创建一个隐藏的iframe,iframe的src指向cnblogs.com域名下的cs2.html页面,这时的hash值可以做参数传递用。cs2.html响应请求后再将通过修改cs1.html的hash值来传递数据(由于两个页面不在同一个域下IE、Chrome不允许修改parent.location.hash的值,所以要借助于a.com域名下的一个代理iframe;Firefox可以修改)。同时在cs1.html上加一个定时器,隔一段时间来判断location.hash的值有没有变化,一有变化则获取获取hash值。
缺点:
9、XDR
10、图像ping(单向)
var img = new Image();
img.onload = img.onerror = function(){
alert("done!");
};
img.src = "https://raw.githubusercontent.com/zhangmengxue/Todo-List/master/me.jpg";
document.body.insertBefore(img,document.body.firstChild);
11、flash