实现跨域必须要知道的知识点

目录

同源策略

cookie

iframe和多窗口通信

片段识别符

window.postMessage()

LocalStorage

Storage接口:

概述

属性和方法

Storage.setItem()

Storage.getItem()

Storage.removeItem()

Storage.clear()

Storage.key()

[storage 事件](#storage 事件)

同源策略

浏览器安全的基石是"同源政策"(same-origin policy)。很多开发者都知道这一点,但了解得不全面。

最初,它的含义是指,A 网页设置的 Cookie,B 网页不能打开,除非这两个网页"同源"。

所谓"同源"指的是"三个相同"。

  • 协议相同

  • 域名相同

  • 端口相同(这点可以忽略,详见下文)

注意:标准规定端口不同的网址不是同源(比如8000端口和8001端口不是同源),但是浏览器没有遵守这条规定。实际上,同一个网域的不同端口,是可以互相读取 Cookie 的。

限制范围

目前,如果非同源,共有三种行为受到限制。

(1) 无法读取非同源网页的 Cookie、LocalStorage 和 IndexedDB。(后面这两个都类似于存储数据的数据库)

(2) 无法接触非同源网页的 DOM。

(3) 无法向非同源地址发送 AJAX 请求(可以发送,但浏览器会拒绝接受响应)。

另外,通过 JavaScript 脚本可以拿到其他窗口的window对象。

如果是非同源的网页,目前允许一个窗口可以接触其他网页的window对象的九个属性和四个方法。

javascript 复制代码
- window.closed
- window.frames
- window.length
- window.location
- window.opener
- window.parent
- window.self
- window.top
- window.window
- window.blur()
- window.close()
- window.focus()
- window.postMessage()

Cookie 是服务器写入浏览器的一小段信息,只有同源的网页才能共享。

如果两个网页一级域名相同,只是次级域名不同,浏览器允许通过设置document.domain共享 Cookie。

举例来说,A 网页的网址是http://w1.example.com/a.html,B 网页的网址是http://w2.example.com/b.html,那么只要设置相同的document.domain,两个网页就可以共享 Cookie

因为浏览器通过document.domain属性来检查是否同源。

javascript 复制代码
// 两个网页都需要设置
document.domain = 'example.com';

注意,A 和 B 两个网页都需要设置document.domain属性,才能达到同源的目的。

因为设置document.domain的同时,会把端口重置为null,因此如果只设置一个网页的document.domain,会导致两个网址的端口不同,还是达不到同源的目的。

现在,A 网页通过脚本设置一个 Cookie。

javascript 复制代码
document.cookie = "test1=hello";

B 网页就可以读到这个 Cookie。

javascript 复制代码
var allCookie = document.cookie; 
//这里就可以读取到前面设置的cookie

注意,这种方法只适用于 Cookie 和 iframe 窗口,LocalStorage 和 IndexedDB 无法通过这种方法,规避同源政策,而要使用 PostMessage API。

另外,服务器也可以在设置 Cookie 的时候,指定 Cookie 的所属域名为一级域名,比如example.com

javascript 复制代码
Set-Cookie: key=value; domain=example.com; path=/

这样的话,二级域名和三级域名不用做任何设置,都可以读取这个 Cookie。

iframe和多窗口通信

iframe元素可以在当前网页之中,嵌入其他网页。

每个iframe元素形成自己的窗口,即有自己的window对象。iframe`窗口之中的脚本,可以获得父窗口和子窗口。

但是,只有在同源的情况下,父窗口和子窗口才能通信;如果跨域,就无法拿到对方的 DOM。

如,父窗口运行下面的命令,如果iframe窗口不是同源,就会报错。

javascript 复制代码
document
.getElementById("myIFrame")
.contentWindow
.document
// Uncaught DOMException: Blocked a frame from accessing a cross-origin frame.

上面命令中,父窗口想获取子窗口的 DOM,因为跨域导致报错。

反之亦然,子窗口获取主窗口的 DOM 也会报错。

javascript 复制代码
window.parent.document.body
// 报错

这种情况不仅适用于iframe窗口,还适用于window.open方法打开的窗口,即,只要跨域,父窗口与子窗口之间就无法通信。

如果两个窗口一级域名相同,只是二级域名不同,那么只要设置document.domain属性,就可以规避同源政策,拿到 DOM。

片段识别符

片段标识符(fragment identifier)指的是,URL 的#号后面的部分,比如http://example.com/x.html#fragment#fragment。如果只是改变片段标识符,页面不会重新刷新

父窗口可以把信息,写入子窗口的片段标识符。

javascript 复制代码
var src = originURL + '#' + data;
//将要写入的信息写入片段标识符中
document.getElementById('myIFrame').src = src;
//这里再获取src

上面代码中,父窗口把所要传递的信息,写入 iframe 窗口的片段标识符。

子窗口通过监听hashchange事件得到通知。

javascript 复制代码
window.onhashchange = checkMessage;
​
function checkMessage() {
  var message = window.location.hash;
  // ...
}

同样的,子窗口也可以改变父窗口的片段标识符。

javascript 复制代码
parent.location.href = target + '#' + hash;

window.postMessage()

上面的这种方法属于破解,HTML5 为了解决这个问题,引入了一个全新的API:跨文档通信 API(Cross-document messaging)。

这个 API 为window对象新增了一个window.postMessage方法,允许跨窗口通信,不论这两个窗口是否同源。举例来说,父窗口aaa.com向子窗口bbb.com发消息,调用postMessage方法就可以了。

javascript 复制代码
// 父窗口打开一个子窗口
var popup = window.open('http://bbb.com', 'title');
// 父窗口向子窗口发消息
popup.postMessage('Hello World!', 'http://bbb.com');

postMessage方法的第一个参数是具体的信息内容,第二个参数是接收消息的窗口的源(origin),即"协议 + 域名 + 端口"。也可以设为*,表示不限制域名,向所有窗口发送。

子窗口向父窗口发送消息的写法类似。

javascript 复制代码
// 子窗口向父窗口发消息
window.opener.postMessage('Nice to see you', 'http://aaa.com');

父窗口和子窗口都可以通过message事件,监听对方的消息。

javascript 复制代码
// 父窗口和子窗口都可以用下面的代码,
// 监听 message 消息
window.addEventListener('message', function (e) {
  console.log(e.data);
},false);

message事件的参数是事件对象event,提供以下三个属性。

  • event.source:发送消息的窗口

  • event.origin: 消息发向的网址

  • event.data: 消息内容

下面的例子是,子窗口通过event.source属性引用父窗口,然后发送消息。

javascript 复制代码
window.addEventListener('message', receiveMessage);
function receiveMessage(event) {
  event.source.postMessage('Nice to see you!', '*');
    //发送消息的窗口    
}

上面代码有几个地方需要注意:

首先,receiveMessage函数里面没有过滤信息的来源,任意网址发来的信息都会被处理。

其次,postMessage方法中指定的目标窗口的网址是一个星号,表示该信息可以向任意网址发送。

通常来说,这两种做法是不推荐的,因为不够安全,可能会被恶意利用。

event.origin属性可以过滤不是发给本窗口的消息。

javascript 复制代码
window.addEventListener('message', receiveMessage);
function receiveMessage(event) {
  if (event.origin !== 'http://aaa.com') return;
    //event.origin: 消息发向的网址
  if (event.data === 'Hello World') {
    //event.data: 消息内容
    event.source.postMessage('Hello', event.origin);
    //event.source:发送消息的窗口
  } else {
    console.log(event.data);
  }
}

LocalStorage

通过window.postMessage,读写其他窗口的 LocalStorage 也成为了可能。

下面是一个例子,主窗口写入 iframe 子窗口的localStorage

javascript 复制代码
window.onmessage = function(e) {
  if (e.origin !== 'http://bbb.com') {
    return;
  }
  var payload = JSON.parse(e.data);
  localStorage.setItem(payload.key, JSON.stringify(payload.data));
};

上面代码中,子窗口将父窗口发来的消息,写入自己的 LocalStorage。

父窗口发送消息的代码如下。

javascript 复制代码
var win = document.getElementsByTagName('iframe')[0].contentWindow;
var obj = { name: 'Jack' };
win.postMessage(
  JSON.stringify({key: 'storage', data: obj}),
  'http://bbb.com'
);

加强版的子窗口接收消息的代码如下。

javascript 复制代码
window.onmessage = function(e) {
  if (e.origin !== 'http://bbb.com') return;
  var payload = JSON.parse(e.data);
  switch (payload.method) {
    case 'set':
      localStorage.setItem(payload.key, JSON.stringify(payload.data));
      break;
    case 'get':
      var parent = window.parent;
      var data = localStorage.getItem(payload.key);
      parent.postMessage(data, 'http://aaa.com');
      break;
    case 'remove':
      localStorage.removeItem(payload.key);
      break;
  }
};

加强版的父窗口发送消息代码如下。

javascript 复制代码
var win = document.getElementsByTagName('iframe')[0].contentWindow;
var obj = { name: 'Jack' };
// 存入对象
win.postMessage(
  JSON.stringify({key: 'storage', method: 'set', data: obj}),
  'http://bbb.com'
);
// 读取对象
win.postMessage(
  JSON.stringify({key: 'storage', method: "get"}),
  "*"
);
window.onmessage = function(e) {
  if (e.origin != 'http://aaa.com') return;
  console.log(JSON.parse(e.data).name);
};

Storage接口:

概述

Storage 接口用于脚本在浏览器保存数据。

两个对象部署了这个接口:window.sessionStoragewindow.localStorage

sessionStorage保存的数据用于浏览器的一次会话(session),当会话结束(通常是窗口关闭),数据被清空;localStorage保存的数据长期存在,下一次访问该网站的时候,网页可以直接读取以前保存的数据。除了保存期限的长短不同,这两个对象的其他方面都一致。

保存的数据都以"键值对"的形式存在。也就是说,每一项数据都有一个键名和对应的值。所有的数据都是以文本格式保存。

这个接口很像 Cookie 的强化版,能够使用大得多的存储空间。 目前,每个域名的存储上限视浏览器而定,Chrome 是 2.5MB,Firefox 和 Opera 是 5MB,IE 是 10MB。其中,Firefox 的存储空间由一级域名决定,而其他浏览器没有这个限制。也就是说,Firefox 中,a.example.comb.example.com共享 5MB 的存储空间。另外,与 Cookie 一样,它们也受同域限制。某个网页存入的数据,只有同域下的网页才能读取,如果跨域操作会报错。

属性和方法

Storage 接口只有一个属性。

  • Storage.length:返回保存的数据项个数。
javascript 复制代码
window.localStorage.setItem('foo', 'a');
window.localStorage.setItem('bar', 'b');
window.localStorage.setItem('baz', 'c');
​
window.localStorage.length // 3

该接口提供5个方法。

Storage.setItem()

Storage.setItem()方法用于存入数据。它接受两个参数,第一个是键名,第二个是保存的数据。如果键名已经存在,该方法会更新已有的键值。该方法没有返回值。

javascript 复制代码
window.sessionStorage.setItem('key', 'value');
window.localStorage.setItem('key', 'value');

注意,Storage.setItem()两个参数都是字符串。如果不是字符串,会自动转成字符串,再存入浏览器。

javascript 复制代码
window.sessionStorage.setItem(3, { foo: 1 });
window.sessionStorage.getItem('3') // "[object Object]"

上面代码中,setItem方法的两个参数都不是字符串,但是存入的值都是字符串。

如果储存空间已满,该方法会抛错。

写入不一定要用这个方法,直接赋值也是可以的。

javascript 复制代码
// 下面三种写法等价
window.localStorage.foo = '123';
window.localStorage['foo'] = '123';
window.localStorage.setItem('foo', '123');
Storage.getItem()

Storage.getItem()方法用于读取数据。它只有一个参数,就是键名。如果键名不存在,该方法返回null

javascript 复制代码
window.sessionStorage.getItem('key')
window.localStorage.getItem('key')

键名应该是一个字符串,否则会被自动转为字符串。

Storage.removeItem()

Storage.removeItem()方法用于清除某个键名对应的键值。它接受键名作为参数,如果键名不存在,该方法不会做任何事情。

javascript 复制代码
sessionStorage.removeItem('key');
localStorage.removeItem('key');
Storage.clear()

Storage.clear()方法用于清除所有保存的数据。该方法的返回值是undefined

javascript 复制代码
window.sessionStorage.clear()
window.localStorage.clear()
Storage.key()

Storage.key()方法接受一个整数作为参数(从零开始),返回该位置对应的键名。

javascript 复制代码
window.sessionStorage.setItem('key', 'value');
window.sessionStorage.key(0) // "key"

结合使用Storage.length属性和Storage.key()方法,可以遍历所有的键。

javascript 复制代码
for (var i = 0; i < window.localStorage.length; i++) {
  console.log(localStorage.key(i));
}
storage 事件

Storage 接口储存的数据发生变化时,会触发 storage 事件,可以指定这个事件的监听函数。

javascript 复制代码
window.addEventListener('storage', onStorageChange);

监听函数接受一个event实例对象作为参数。这个实例对象继承了 StorageEvent 接口,有几个特有的属性,都是只读属性。

  • StorageEvent.key:字符串,表示发生变动的键名。如果 storage 事件是由clear()方法引起,该属性返回null

  • StorageEvent.newValue:字符串,表示新的键值。如果 storage 事件是由clear()方法或删除该键值对引发的,该属性返回null

  • StorageEvent.oldValue:字符串,表示旧的键值。如果该键值对是新增的,该属性返回null

  • StorageEvent.storageArea:对象,返回键值对所在的整个对象。也说是说,可以从这个属性上面拿到当前域名储存的所有键值对。

  • StorageEvent.url:字符串,表示原始触发 storage 事件的那个网页的网址。

下面是StorageEvent.key属性的例子。

javascript 复制代码
function onStorageChange(e) {
  console.log(e.key);
}
​
window.addEventListener('storage', onStorageChange);

注意,该事件有一个很特别的地方,就是它不在导致数据变化的当前页面触发,而是在同一个域名的其他窗口触发。也就是说,如果浏览器只打开一个窗口,可能观察不到这个事件。比如同时打开多个窗口,当其中的一个窗口导致储存的数据发生改变时,只有在其他窗口才能观察到监听函数的执行。可以通过这种机制,实现多个窗口之间的通信。

相关推荐
css趣多多5 分钟前
案例自定义tabBar
前端
天上掉下来个程小白12 分钟前
案例-14.文件上传-简介
数据库·spring boot·后端·mybatis·状态模式
哆木22 分钟前
排查生产sql查询缓慢
数据库·sql·mysql
AI服务老曹22 分钟前
运用先进的智能算法和优化模型,进行科学合理调度的智慧园区开源了
运维·人工智能·安全·开源·音视频
橘子师兄1 小时前
分页功能组件开发
数据库·python·django
shimly1234561 小时前
tcpdump 用法示例
网络·测试工具·tcpdump
林的快手1 小时前
CSS列表属性
前端·javascript·css·ajax·firefox·html5·safari
网络安全King2 小时前
华为 网络安全 认证
安全·web安全
book01212 小时前
MySql数据库运维学习笔记
运维·数据库·mysql
纠结哥_Shrek2 小时前
Oracle和Mysql的区别
数据库·mysql·oracle