我之前一直搞不懂跨域,ajax都理解错了,这让我很受伤,所以现在让我理一理,如果说错了欢迎补充!
1.首先我们说一下ajax是什么?
1) AJAX 是一种在无需重新加载整个网页的情况下,能够更新部分网页的技术。(注意!ajax不是跨域方式!它是一种技术)
ajax= Asynchronous JavaScript and XML(异步的 JavaScript 和 XML)
我理解的ajax就是能请求接口并且部分更新数据。浏览器使用“同源策略”限制了AJAX技术获取数据的范围和能力,不同域之间无法通过AJAX技术获取资源。这是需要跨域获取资源的主要情景。
2)XMLHttpequest 是 AJAX 的基础
XMLHttpRequest 用于在后台与服务器交换数据。这意味着可以在不重新加载整个网页的情况下,对网页的某部分进行更新。
3)ajax实现方式
a.创建XMLHttpRequest 对象
b.向服务器发送请求
如需将请求发送到服务器,我们使用 XMLHttpRequest 对象的 open() 和 send() 方法:
c.服务器响应
获得来自服务器的响应,可以使用 XMLHttpRequest 对象的 responseText 或 responseXML 属性。
d.onreadystatechange 事件,使用 Callback 函数
当请求被发送到服务器时,我们需要执行一些基于响应的任务。
每当 readyState 改变时,就会触发 onreadystatechange 事件。
onreadystatechange 事件被触发 5 次(0 - 4),对应着 readyState 的每个变化。
2.同源是什么?
1) 整个互联网世界的数据要么存储在 服务端 (即服务器,如数据库,硬盘等)中,要么存储在 客户端 (即浏览器,如cookie,LocalStorage,sessionStorage)中。互联网数据的传输实际上就是客户端与服务端之间的交互。
2)浏览器所遵守的“同源策略”是指:限制不同源之间执行特定操作。
这涉及到两个问题:什么是“源”?,以及“特定操作”是指什么?
一个源由协议,域名和端口三部分组成,这三者任一一个不同都会被浏览器识别为不同的源; 特定操作是指:
a.读取 Cookie,LocalStorage 和 IndexDB;
b.获取 DOM 元素和js对象;
c .发送 AJAX 请求;(浏览器的“同源策略” 只是阻止了通过AJAX技术跨域获取资源,而并没有禁止跨域获取资源这件事本身)
3) 所以如果没有同源策略会怎么样
a.没有同源策略限制的接口请求
有一个小小的东西叫cookie大家应该知道,一般用来处理登录等场景,目的是让服务端知道谁发出的这次请求。如果你请求了接口进行登录,服务端验证通过后会在响应头加入Set-Cookie字段,然后下次再发请求的时候,浏览器会自动将cookie附加在HTTP请求的头字段Cookie中,服务端就能知道这个用户已经登录过了。知道这个之后,我们来看场景:
比如你在看微信时打开了另外一个不法网站,由于没有同源策略的限制,他向微信发起了请求,就像上面说的服务端验证通过后会在响应头加入Set-Cookie字段,然后下次再发请求的时候,浏览器会自动将cookie附加在HTTP请求的头字段Cookie中,这样一来,这个不法网站就相当于登录了你的账号,这就是传说中的CSRF攻击。由于cookie是明文的,像XSS攻击就可以去获取到cookie,这样还是很不安全,但由于cookie、Cookie/Session的机制与安全,在服务端可以设置httpOnly,使得前端无法操作cookie;设置secure,则保证在https的加密通信中传输以防截获。
b.没有同源策略限制的Dom查询
有人给你发了一个和一摸一样界面的伪银行网站www.yinghang.com,告诉你账号有风险,你登录进去,我们看看这时候这个网站做了什么?实际网站为ww.yinhang.com
// HTML
<iframe name="yinhang" src="www.yinhang.com"></iframe>
// JS
// 由于没有同源策略的限制,钓鱼网站可以直接拿到别的网站的Dom
const iframe = window.frames['yinhang']
const node = iframe.document.getElementById('你输入账号密码的Input')
console.log(`拿到了这个${node},我还拿不到你刚刚输入的账号密码吗`)
由此我们知道,同源策略确实能规避一些危险,不是说有了同源策略就安全,只是说同源策略是一种浏览器最基本的安全机制,毕竟能提高一点攻击的成本。
3.最最重要的来了: 跨域方式!
1)JSONP
<script>和<img>是本身就不受跨域限制的两个标签
通过script标签引入一个js文件,这个js文件载入成功后会执行我们在url参数中指定的函数,并且会把我们需要的json数据作为参数传入。所以jsonp是需要服务器端的页面进行相应的配合的。
实现方式有原生js,jquery ajax,vuejs(promise执行回调函数)
1.)原生实现:
<script>
var script = document.createElement('script');
script.type = 'text/javascript';
// 传参并指定回调执行函数为onBack
script.src = 'http://www.domain2.com:8080/login?user=admin&callback=onBack';
document.head.appendChild(script);
// 回调执行函数
function onBack(res) {
alert(JSON.stringify(res));
}
</script>
服务端返回如下(返回时即执行全局函数):
onBack({"status": true, "user": "admin"})
2.)jquery ajax:
$.ajax({
url: 'http://www.domain2.com:8080/login',
type: 'get',
dataType: 'jsonp', // 请求方式为jsonp
jsonpCallback: "onBack", // 自定义回调函数名
data: {}
});
3.)vue.js:
this.$http.jsonp('http://www.domain2.com:8080/login', {
params: {},
jsonp: 'onBack'
}).then((res) => {
console.log(res);
})
后端node.js代码示例:
var querystring = require('querystring');
var http = require('http');
var server = http.createServer();
server.on('request', function(req, res) {
var params = qs.parse(req.url.split('?')[1]);
var fn = params.callback;
// jsonp返回设置
res.writeHead(200, { 'Content-Type': 'text/javascript' });
res.write(fn + '(' + JSON.stringify(params) + ')');
res.end();
});
server.listen('8080');
console.log('Server is running at port 8080...');
缺点:这种方式无法发送post请求(这里)
JSONP技术与AJAX技术无关:虽然同样牵扯到跨域获取资源这个主题,但我们应该已经清楚的看到,JSONP的本质是绕过AJAX获取资源的机制,使用原始的src属性获取异域资源
那如何结合jsonp和ajax跨域
2)跨域资源共享(CORS)
http://www.ruanyifeng.com/blog/2016/04/cors.html
它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。只要服务器实现了CORS接口,就可以跨源通信。对于简单请求,浏览器直接发出CORS请求。具体来说,就是在头信息之中,增加一个Origin字段。
Origin字段用来说明,本次请求来自哪个源(协议 + 域名 + 端口)。服务器根据这个值,决定是否同意这次请求
普通跨域请求:只服务端设置Access-Control-Allow-Origin即可,前端无须设置,若要带cookie请求:前后端都需要设置。
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
Access-Control-Max-Age: 86400
跨域请求默认不会携带Cookie信息,如果需要携带,请配置下述参数:
"Access-Control-Allow-Credentials": true
// Ajax设置
"withCredentials": true
3) Nodejs中间件代理跨域
node中间件实现跨域代理,原理大致与nginx相同,都是通过启一个代理服务器,实现数据的转发,也可以通过设置cookieDomainRewrite参数修改响应头中cookie中域名,实现当前域的cookie写入,方便接口登录认证。
4)通过修改document.domain来跨子域,只适用于不同子域的框架间的交互。
如果你想通过ajax的方法去与不同子域的页面交互,除了使用jsonp的方法外,还可以用一个隐藏的iframe来做一个代理。原理就是让这个iframe载入一个与你想要通过ajax获取数据的目标页面处在相同的域的页面,所以这个iframe中的页面是可以正常使用ajax去获取你要的数据的,然后就是通过我们刚刚讲得修改document.domain的方法,让我们能通过js完全控制这个iframe,这样我们就可以让iframe去发送ajax请求,然后收到的数据我们也可以获得了。
前提条件:这两个域名必须属于同一个基础域名!而且所用的协议,端口都要一致,否则无法利用document.domain
进行跨域,所以只能跨子域
在根域范围内,允许把domain属性的值设置为它的上一级域。例如,在”aaa.xxx.com”域内,可以把domain设置为 “xxx.com” 但不能设置为 “xxx.org” 或者”com”。
现在存在两个域名aaa.xxx.com和bbb.xxx.com。在aaa下嵌入bbb的页面,由于其
document.name
不一致,无法在aaa下操作bbb的js。可以在aaa和bbb下通过js将document.name = 'xxx.com';
设置一致,来达到互相访问的作用。
5)使用window.name来进行跨域
window对象有个name属性,该属性有个特征:即在一个窗口(window)的生命周期内,窗口载入的所有的页面都是共享一个window.name的,每个页面对window.name都有读写的权限,window.name是持久存在一个窗口载入过的所有页面中的,并不会因新页面的载入而进行重置。
使用一个隐藏的iframe来充当一个中间人角色,由iframe去获取data.html的数据,然后a.html再去得到iframe获取到的数据。
var proxy = function(url, callback) {
var state = 0;
var iframe = document.createElement('iframe');
// 加载跨域页面
iframe.src = url;
// onload事件会触发2次,第1次加载跨域页,并留存数据于window.name
iframe.onload = function() {
if (state === 1) {
// 第2次onload(同域proxy页)成功后,读取同域window.name中数据
callback(iframe.contentWindow.name);
destoryFrame();
} else if (state === 0) {
// 第1次onload(跨域页)成功后,切换到同域代理页面
iframe.contentWindow.location = 'http://www.domain1.com/proxy.html';
state = 1;
}
};
document.body.appendChild(iframe);
// 获取数据以后销毁这个iframe,释放内存;这也保证了安全(不被其他域frame js访问)
function destoryFrame() {
iframe.contentWindow.document.write('');
iframe.contentWindow.close();
document.body.removeChild(iframe);
}
};
// 请求跨域b页面数据
proxy('http://www.domain2.com/b.html', function(data){
alert(data);
});
6)使用HTML5中新引进的window.postMessage方法来跨域传送数据适用于两个异域客户端
可以使用它来向其它的window对象发送消息,无论这个window对象是属于同源或不同源
调用postMessage方法的window对象是指要接收消息的那一个window对象,该方法的第一个参数message为要发送的消息,类型只能为字符串;第二个参数targetOrigin用来限定接收消息的那个window对象所在的域,如果不想限定域,可以使用通配符 * 。
需要接收消息的window对象,可是通过监听自身的message事件来获取传过来的消息,消息内容储存在该事件对象的data属性中。
让我们用具体的业务场景与代码进一步说明,假如我们的页面现在有两个窗口,窗口1命名为“window_1”, 窗口2命名为“window_2”,当然,窗口1与窗口2的“域”是不同的,我们的需求是由窗口1向窗口2发送数据,而当窗口2接收到数据时,将数据再返回给窗口1。先让我们看看窗口1script
标签内的代码:
// window_1 域名为 http://winodow1.com:8080
window.postMessage("Hi, How are you!", "http://window2.com:8080")
可以看到,postMessage
函数接收两个参数,第一个为要发送的信息(可以是任何JavaScript类型数据,但部分浏览器只支持字符串格式),第二个为信息发送的目标地址。让我们再看看窗口2script
标签内的代码:
// window_2 域名为 http://window2.com:8080
window.addEventListener("message", receiveMessage, false)
function receiveMessage(event) {
// 对于Chorme,origin属性为originalEvent.origin属性
var origin = event.origin || event.originalEvent.origin
if (origin !== "http://window1.com:8080") {
return
}
window.postMessage("I\'m ok", "http://window1.com:8080")
}
看到了吗,我们在window上绑定了一个事件监听函数,监听message
事件。一旦我们接收到其他域通过postMessage
发送的信息,就会触发我们的receiveMessage
回调函数。该函数会首先检查发送信息的域是否是我们想要的(之后我们会对此详细说明),如果验证成功则会像窗口1发送一条消息。
7) WebSocket
是一种 在单个TCP连接上进行全双工通讯的协议 。 为了弥补在 某些情景 下使用HTTP协议通信的一些不足。 某些情景 具体是指“ 服务端与客户端的双向通信 ”。三次握手后 服务器向客户端派发信使与浏览器互发信息,转发资源。 当客户端与服务端创建WebSocket连接后,本身就可以天然的实现跨域资源共享 ,WebSocket协议本身就不受浏览器“同源策略”的限制
让我们也同样构建一个基于WebSocket协议的心智模型,在这个心智模型中,服务端扮演的角色发生了一些改变,服务端不再只是一个“守门人”,同时它也运营着一个和客户端一样的“邮局”,也就是说,他也拥有了可以向客户端发送数据的能力。至此一个完整的基于WebSocket协议的通信流程为:
客户端派发一个信使向服务器送信,服务器扮演的“守门人”检查信件,发现信件中写到“让我们用更加潮流的WebSocket方式交流吧”,服务器在在信件末尾添加上一句“没问题,浏览器伙计”,让信使原路返回告知浏览器。当浏览器再次向服务器告知收到消息时(第三次握手),服务器就开始运转“邮局”,向客户端派发信使与浏览器互发信息,转发资源。
下面是客户端告知服务端要升级为WebSocket协议的报头:
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Origin: http://example.com
下面是服务端向客户端返回的响应报头:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat