跨域解决

跨域解决

什么导致了跨域

跨域

首先,网络是没有绝对安全可言的,但是,我们又需要使用浏览器来访问网络,所以浏览器能存在的安全基础就是有相对较高的安全性,提升了别人做坏事的成本。

Same-origin policy(同源策略,以下简写为 CORS)就是浏览器安全的一个重要部分。

同源策略是一个重要的安全策略,它用于限制一个源(Origin)的文档或者它加载的脚本如何能与另一个源的资源进行交互。它能帮助阻隔恶意文档,减少可能被攻击的媒介。

同源策略的目的有两个:

  • 对系统用户:保证用户信息的安全,防止恶意的网站窃取数据
  • 对开发人员:约束网站使用的资源尽可能是同源可控或者信任的资源,减少问题的可能性,从而来增强网站的安全性

真实案例(仅限演示例子使用):
在登录腾讯文档后,打开一个文档,在控制台调用下面的代码,将会给一个好友分配该文档的编辑权限。

1
2
3
4
5
6
7
8
9
$.ajax({
type: "post",
url: "https://docs.qq.com/cgi-bin/redirect/300000000/ep/api/setpadinfo?localPadId=BYtWkVVnlVCW&type=1&ver=2&route_ip=&room_route_ip=&get_vip_info=1",
data: "corp_id=&data=%7B%22policy%22%3A1%2C%22addmemlist%22%3A%5B%7B%22uintype%22%3A0%2C%22uin%22%3A2803621806%2C%22work_id%22%3A%22%22%2C%22info%22%3A2%2C%22new%22%3A1%7D%5D%2C%22submemlist%22%3A%5B%5D%7D&message=%7B%22seq%22%3A%2272bcde05-6052-44f2-be8c-b6454a3e6716%22%2C%22action%22%3A1%7D&xsrf=ac8bd23b7c50fa4b&dataType=0",
success: function (data) {
console.log(data);
},
error: function (err) {},
});

退出账号后,调用相同代码,不能给相同好友分配该文档的编辑权限。并报下面的错误。

1
2
3
cgicode: 11000;
msg: "no correct p_uin or p_skey or uid or uid_key in cookie [errcode:11000:0]";
retcode: 11000;

由报错可以看出,出现问题是因为 cookie 中缺少相关信息,导致服务端认证失败。

在其他网站的控制台调用相同代码会报如下错误:

1
Access to XMLHttpRequest at 'https://docs.qq.com/cgi-bin/redirect/300000000/ep/api/setpadinfo?localPadId=BYtWkVVnlVCW&type=1&ver=2&route_ip=&room_route_ip=&get_vip_info=1' from origin 'https://fanyi.baidu.com' has been blocked by CORS policy: 、No 'Access-Control-Allow-Origin' header is present on the requested resource.

现在根据上面的情况试想在没有同源策略的情况下:
在你登陆腾讯文档后,再打开一个其他源的网站,调用上面的代码,也是会成功给好友分配编辑权限的。进而联想到在一个第三方网站掌握腾讯文档的接口信息规则后,就可以假冒你对你的账号进行任意操作,这是十分可怕的。

那么怎么判断是同一个源呢,它的定义是什么呢?

同源的定义:
如果两个 URL 的协议主机 和  端口 (如果有指定的话) 都相同的话,则这两个 URL 是同源。这个方案也被称为“协议/主机/端口元组”。

注:

  1. 只有浏览器才会有跨域问题,因为只有浏览器才有同源策略
  2. 恶意文档:可能导致推栈缓冲区溢出,从而在电脑中执行一些代码的文件,一般指病毒或者木马的运行程序。

跨域示例

下表给出了与 URL:http://store.company.com/dir/page.html  的源进行对比的示例:

URL 结果 原因
http://store.company.com/dir2/other.html 同源 只有路径不同
http://store.company.com/dir/inner/another.html 同源 只有路径不同
https://store.company.com/secure.html 失败 协议不同
http://store.company.com:81/dir/etc.html 失败 端口不同 ( http:  默认端口是 80;https: 默认端口是 443)
http://news.company.com/dir/other.html 失败 主机不同

注:域名和 IP 指向一样,但是还是会引起跨域的。
例如:http://www.ths.com.cn/http://223.223.179.206/都指向同一个地方

限制范围

随着互联网的发展,”同源策略”越来越严格。目前,如果非同源,共有三种行为受到限制。

  • Cookie、LocalStorage、SessionStorage 和 IndexDB 无法读取。
  • 脚本 API 访问。
  • AJAX 请求不能发送。

常见跨域错误

缺少可跨域响应头

关键字: No ‘Access-Control-Allow-Origin’ header (Access-Control-Allow-Origin 翻译:允许访问的源)
注:客户端发送的叫请求,服务器端返回的叫响应
生动例子:客户端向服务器借钱是请求,服务器不给钱是对客户端的响应

允许跨域的值重复

关键字: The ‘Access-Control-Allow-Origin’ header contains multiple values ‘*, *‘, but only one is allowed。 (”允许访问的源“响应头包含重复的两个*号,但是只有一个是允许的)
原因: 多次代理的时候配置的允许跨域的值有重复的

不允许访问其他域的对象或者数据存储(cookie 等)

关键字:block a frame with origin form accessing a cross-origin frame (访问了一个跨域的源)

解决跨域

跨域网络访问(Cross-origin network access)

因为跨域产生的原因是两个资源不在同一个域,所以有四种解决的办法:

  • 把两个资源放到同一个域中
  • 服务器允许资源跨域共享(CORS)
  • 浏览器插件
  • 利用一些不受同源策略影响的标签实现(script

把两个资源放到同一个域中

  • 反向代理:通过 nginx 把多个资源代理到一块
    • 参考 nginx 说明文档
      注: 在能操作文件的情况下,不建议多次代理,因为这样会导致网络传输变慢,影响系统的流畅性。
  • 正向代理:通过同域的接口返回其他域的资源

注:

  • 反向代理:访问自定义的接口来返回其他接口的资源,通过自定义接口已经不能分出真实资源来自哪里
  • 正向代理:由代理服务器去请求资源并返回给你,访问还是原来的真实网址
  • 不管是正向代理还是反向代理,都是由服务器端去进行访问的,所以要保证在服务器端能访问到真实地址

服务器允许资源跨域共享

  • Tomcat

在 Tomcat 根目录 –> conf –> web.xml 的 web-app 节点下加入如下代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<filter>
<filter-name>CorsFilter</filter-name>
<filter-class>org.apache.catalina.filters.CorsFilter</filter-class>
<init-param>
<param-name>cors.allowed.origins</param-name>
<param-value>*</param-value>
<!-- 当指定部分可跨域时,使用下面代码配置指定域 -->
<!-- <param-value>http://127.0.0.1:10229</param-value> -->
</init-param>
</filter>
<filter-mapping>
<filter-name>CorsFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

注: Tomcat 配置修改后必须重启 Tomcat 才会生效

  • Nginx
    在 Nginx 的根目录 –> conf –> nginx.conf 文件适当写入下述代码。
    • 全局可跨域
1
2
3
4
5
# 在 server 节点下添加以下语句
add_header Access-Control-Allow-Origin *;
add_header 'Access-Control-Allow-Credentials' 'true';
add_header Access-Control-Allow-Methods 'GET,POST';
add_header Access-Control-Allow-Headers 'Content-Type,*';
  • 指定接口可跨域
1
2
3
4
5
6
7
8
# 在指定的 location 节点下添加,示例如下:
location /tomcat/ {
proxy_pass http://localhost:8090/;
add_header Access-Control-Allow-Origin *;
add_header 'Access-Control-Allow-Credentials' 'true';
add_header Access-Control-Allow-Methods 'GET,POST';
add_header Access-Control-Allow-Headers 'Content-Type,*';
}
注:
  • Nginx 配置修改后必须重启才会生效。
  • 同时添加全局可跨域和指定接口可跨域时,访问指定接口会产生可跨域响应头重复的问题,可使用指定接口代理,因此不推荐使用全局可跨域
  • 注意localtion的匹配规则
  • 当配置指定域可跨域时,可把*换为具体的域名或者 IP,多个之间用逗号隔开。
  • 多次代理设置具体相同的域也是会报允许跨域的值重复错误
  • 多次代理设置域时,http:127.0.0.1:10229/ === http:127.0.0.1:10229

补:location 和 proxy_pass 易混淆点

  • location (后面两点为 URL 访问的相关知识补充)

    • location 后面带不带/ 都是一样的

    • 如果 URL 的格式为http://my.suyp.com/或者http://127.0.0.1:10229/,**尾部有没有/都不会造成重定向**。
      因为这种情况下,浏览器请求时会在后面默认加上/。可以这样理解,没有请求是访问的http://my.suyp.com,都是访问的http://my.suyp.com/,所以没有重定向。

    • 如果 URL 的格式为http:127.0.1:10229/node/。尾部如果缺少/将导致重定向
      因为根据约定,URL 尾部的/表示目录,没有/表示文件。所以访问/node/时,服务器会自动去该目录下找对应的默认文件或者返回该目录的文件列表。如果访问/node的话,服务器会先去找node文件,找不到的话会将node当成目录重定向到/node/,去该目录下找默认文件或者返回该目录的文件列表。

  • proxy_pass
    假设下面四种情况分别用 http://192.168.1.1/proxy/test.html 进行访问。

    -

    1
    2
    3
    4
    location /proxy/ {
    proxy_pass http://127.0.0.1:10229/;
    }
    # 代理到URL:http://127.0.0.1:10229/test.html

    -

    1
    2
    3
    4
    location /proxy/ {
    proxy_pass http://127.0.0.1:10229; # (相对于第一种,最后少一个 / )
    }
    # 代理到URL:http://127.0.0.1:10229/test.html

    -

    1
    2
    3
    4
    location /proxy/ {
    proxy_pass http://127.0.0.1:10229/aaa/;
    }
    # 代理到URL:http://127.0.0.1:10229/aaa/test.html

    -

    1
    2
    3
    4
    location /proxy/ {
    proxy_pass http://127.0.0.1:10229/aaa; # (相对于第三种,最后少一个 / )
    }
    # 代理到URL:http://127.0.0.1:10229/aaatest.html

    理解:
    如果最后有/,就是把 URL 以location路由切割,把后面的部分放到代理地址的后面,如果没有,就是把路由加上后面的部分放到代理地址的后面。(注意: http://127.0.0.1===http://127.0.0.1/)。

推荐配置代理的写法:

1
2
3
4
5
6
7
8
9
location /proxy/ {
proxy_pass http://127.0.0.1/;
}
location /proxy/ {
proxy_pass http://127.0.0.1/aaa/;
}
# location 的路由最后也加上/
# proxy_pass 的最后也加上/
# 好处:访问的地址和真实地址在/proxy/之后是完全一样的,便于理解记忆
  • Node
    • 全局可跨域
1
2
3
4
5
6
7
8
9
app.all("*", (req, res, next) => {
// 设置允许跨域的域名,*代表允许任意域名跨域
res.header("Access-Control-Allow-Origin", "*");
// 允许的header类型
res.header("Access-Control-Allow-Headers", "content-type");
// 跨域允许的请求方式
res.header("Access-Control-Allow-Methods", "DELETE,PUT,POST,GET,OPTIONS");
next();
});
  • 指定接口可跨域
1
2
3
4
5
6
7
8
app.get("/node", (req, res) => {
// 设置允许跨域的域名,*代表允许任意域名跨域
res.header("Access-Control-Allow-Origin", "*");
// 允许的header类型
res.header("Access-Control-Allow-Headers", "content-type");
// 跨域允许的请求方式
res.header("Access-Control-Allow-Methods", "DELETE,PUT,POST,GET,OPTIONS");
});

注:因为 Node 的路由是由上到下匹配的,有符合的默认就不继续向下匹配了 + 指定全局可跨域时,一定要先写app.all + 在app.all 中,一定要加next(), 来让路由继续向下匹配

浏览器插件

在 chrome 网上商店中搜索 Allow CORS: Access-Control-Allow-Origin 插件安装,在需要的时候运行即可。

利用一些不受同源策略影响的标签

JSONP

JSONP 是服务器与客户端跨源通信的一种方法。最大特点就是简单适用,老式浏览器全部支持,服务器不用做任何改造。

理论基础:Web 页面上调用 js 文件时是不受否跨域的影响(不仅如此,凡是拥有”src”这个属性的标签都拥有跨域的能力,比如<script><img><iframe>)。

因为 JSONP 是利用script标签的特性来实现跨域的,所以不支持post请求。

基本思想:在远程服务器上设法把数据装进 js 格式的文件里,供客户端调用和进一步处理。

首先,网页动态插入<script>元素,由它向跨源网址发出请求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function addScriptTag(src) {
var script = document.createElement("script");
script.setAttribute("type", "text/javascript");
script.src = src;
document.body.appendChild(script);
}

window.onload = function () {
// 这里使用一个文本来模拟jsonp服务端返回的最终数据格式
// 文本内容:jsonpExe({"data":"我是jsonp的数据"});
addScriptTag("http://localhost:1000/jsonp.txt?callback=jsonpExe");
};

function jsonpExe(param) {
// jsonp方式传回来的数据本身就是json对象
alert(JSON.stringify(param));
}

上面代码通过动态添加<script>元素,向服务器localhost:1000发出请求。注意,该请求的查询字符串有一个callback参数,用来指定回调函数的名字,这对于 JSONP 是必需的。

服务器收到这个请求以后,会将数据放在回调函数的参数位置返回。

1
jsonpExe({ data: "我是jsonp的数据" });

由于<script>元素请求的脚本,直接作为代码运行。这时,只要浏览器定义了foo函数,该函数就会立即调用。作为参数的 JSON 数据被视为 JavaScript 对象,而不是字符串,因此避免了使用JSON.parse的步骤。

或者使用 AJAX 调用,示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function getJSONPData() {
$.ajax({
type: "get",
url: "http://localhost:1000/jsonp.txt",
dataType: "jsonp", // 一定要使用 jsonp 类型
success: function (data) {
console.log(data);
},
error: function (err) {},
});
}

function jsonpExe(param) {
// jsonp方式传回来的数据本身就是json对象
alert(JSON.stringify(param));
}

表单提交数据不受同源策略的影响

  • 表单提交:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- 成功 -->
<form action="http://localhost:10229/setUserInfo" method="post">
<p>
<label>用户名:</label>
<input type="text" name="user" />
</p>
<p>
<label>密&nbsp;&nbsp;&nbsp;码:</label>
<input type="password" name="password" />
</p>
<p>
<input type="submit" name="" value="提交1">
<input type="reset" name="" value="重置1">
</p>
</form>
  • ajax 请求:
1
2
3
4
5
6
7
8
9
10
11
12
13
// err 跨域
$.ajax({
type: "post",
url: "http://localhost:10229/setUserInfo",
data: {
user: "suyp",
},
success: function (data) {
console.log(data);
$(".txt-erea").text(data);
},
error: function (err) {},
});

发散一下思维:
虽然form标签只能发送数据,但是没有跨域问题,所以在只需要发送消息的时候也可以使用form来进行单方面通信。

Canvas 中的跨域问题

受影响的方法如下:

  • getImageData():
    返回一个ImageData对象,用来描述 canvas 区域隐含的像素数据
  • toBlob():
    创造Blob对象,用以展示 canvas 上的图片;这个图片文件可以被缓存或保存到本地,由用户代理端自行决定。如不特别指明,图片的类型默认为 image/png,分辨率为96dpi
  • toDataURL() :
    返回一个包含图片展示的 data URI 。可以使用 type 参数其类型,默认为 PNG 格式。图片的分辨率为96dpi

下面以toDataURL()为例。

在使用Canvas绘制不同域的图片然后转为Base64时,会有跨域问题。但是,不影响 Canvas 绘制展示图片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<body>
<canvas id="myCanvas" width="1920" height="1080" style="border: 2px solid grey">当前浏览器不支持canvas</canvas>
</body>
<script>
var canvas = document.getElementById("myCanvas")
var context = canvas.getContext("2d")

var img = new Image()
// img.src = './img/思路logo紫.png'; // 同源图片
img.src = 'http://localhost:8090/CORS/思路logo蓝.png'; // 不同源图片
//图片加载完后,将其显示在canvas中
img.onload = function() {
context.drawImage(this, 0, 0, 1920, 1080) // 改变图片大小到1080*980
// toDataURL是向canvas转为Base64的一个方法
console.log(canvas.toDataURL('image/png'));
}
</script>

报错:

解决方式

  1. 设置图片的crossOrigin = 'anonymous',来让toDataURL方法不因跨域报错
  2. 设置图片可跨域
    二者缺一不可
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<body>
<canvas id="myCanvas" width="1920" height="1080" style="border: 2px solid grey">当前浏览器不支持canvas</canvas>
</body>
<script>
var canvas = document.getElementById("myCanvas")
var context = canvas.getContext("2d")

var img = new Image()
img.src = 'http://localhost:8090/CORS/思路logo蓝.png'; // 不同源图片
img.crossOrigin = 'anonymous';
//图片加载完后,将其显示在canvas中
img.onload = function() {
context.drawImage(this, 0, 0, 1920, 1080) // 改变图片大小到1080*980
// toDataURL是向canvas转为Base64的一个方法
console.log(canvas.toDataURL('image/png'));
}
</script>

不允许 IFrame 被嵌入

在响应头中有这么一个配置项 X-Frame-Options来标识一个页面是否可以被其他页面嵌入。
可选值:

  • deny:表示该页面不允许在 frame 中展示,即便是在相同域名的页面中嵌套也不允许。
  • sameorigin:表示该页面可以在相同域名页面的 frame 中展示。
  • allow-from uri:表示该页面可以在指定来源的 frame 中展示。(uri 为指定源的地址)

配置:

  • Tomcat
    在 Tomcat 根目录 –> conf –> web.xml 的 web-app 节点下加入如下代码。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
        <!-- 配置页面是否能被其他页面展示 -->
    <filter>
    <filter-name>httpHeaderSecurity</filter-name>
    <filter-class>org.apache.catalina.filters.HttpHeaderSecurityFilter</filter-class>
    <init-param>
    <param-name>antiClickJackingOption</param-name>
    <!-- 配置的具体值 -->
    <param-value>sameorigin</param-value>
    </init-param>
    <async-supported>true</async-supported>
    </filter>
    <filter-mapping>
    <filter-name>httpHeaderSecurity</filter-name>
    <url-pattern>/*</url-pattern>
    <dispatcher>REQUEST</dispatcher>
    <dispatcher>FORWARD</dispatcher>
    </filter-mapping>
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
        <!-- 配置页面是否能被其他页面展示 -->
    <filter>
    <filter-name>httpHeaderSecurity</filter-name>
    <filter-class>org.apache.catalina.filters.HttpHeaderSecurityFilter</filter-class>
    <init-param>
    <param-name>antiClickJackingOption</param-name>
    <!-- 配置的具体值 -->
    <param-value>allow-from</param-value>
    </init-param>
    <!-- 当指定部分域可展示时,使用下面代码配置指定域 -->
    <init-param>
    <param-name>antiClickJackingUri</param-name>
    <param-value>http://127.0.0.1:10229</param-value>
    </init-param>
    <async-supported>true</async-supported>
    </filter>
    <filter-mapping>
    <filter-name>httpHeaderSecurity</filter-name>
    <url-pattern>/*</url-pattern>
    <dispatcher>REQUEST</dispatcher>
    <dispatcher>FORWARD</dispatcher>
    </filter-mapping>

    注:目前测试指定域可嵌入无效,错误如下:

    1
    testIframe.html:1 Invalid 'X-Frame-Options' header encountered when loading 'http://localhost:8090/CORS/testIframe.html': 'ALLOW-FROM http://127.0.0.1:10229' is not a recognized directive. The header will be ignored.
  • Nginx
    配置 nginx 发送 X-Frame-Options 响应头,把下面这行添加到 ‘http’, ‘server’ 或者 ‘location’ 的配置中:
    add_header X-Frame-Options sameorigin always;
    add_header X-Frame-Options deny always;
    add_header X-Frame-Options allow-from 'http://127.0.0.1,http://127.0.0.2' always;
    小结:
    跨域网络访问推荐做法:

  • 在能随意操作文件的情况优先把资源分类放到一起,这样既不会有跨域请求问题,也方便管理。

  • 不行的话,使用 Nginx 代理。

跨域数据存储访问(Cross-origin data storage access)

使用域名

document.domain 属性中存入的是主机信息,并且可以设置为当前域或者当前域的父域。如果将其设置为其当前域的父域,则这个较短的父域将用于后续源检查
另外,任何对 document.domain 的赋值操作,包括 document.domain = document.domain 都会导致端口号被重写为 null。因此 company.com:8080 不能仅通过设置 document.domain = “company.com” 来与 company.com 通信。必须在他们双方中都进行赋值,以确保端口号都为 null 。

由于以上的情况,我们可以在多个只有二级域名不同的网页进行以下操作。

注:

Cookie 是服务器写入浏览器的一小段信息,只有同源的网页才能共享。但是,两个网页一级域名相同,只是二级域名不同,浏览器允许通过设置 document.domain共享 Cookie

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

1
document.domain = "example.com";

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

1
document.cookie = "test1=hello";

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

1
var allCookie = document.cookie;

另外,服务器也可以在设置 Cookie 的时候,指定 Cookie 的所属域名为一级域名,这样二级域名和三级域名不用做任何设置,都可以读取这个 Cookie。

DOM

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

父页面:

1
2
3
4
window.onload = function () {
document.domain = "suyp.com";
document.cookie = "username=suyp";
};

子页面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
window.onload = function () {
document.domain = "suyp.com";
};

// 这里请求时,会带上父页面的cookie: document.cookie = 'username=suyp';
function getData() {
$.ajax({
type: "get",
url: "http://localhost:10229/node",
// 默认情况下,标准的跨域请求是不会发送cookie的
xhrFields: {
withCredentials: true,
},
success: function (data) {
console.log(data);
// $('.txt-erea').text(data);
},
error: function (err) {},
});
}

Node 服务端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 在开启服务的代码基础上新加如下代码
const express = require("express");
const app = express();
const cookieParser = require("cookie-parser");

app.use(cookieParser());

app.get("/node", (req, res) => {
// 使用cookie时不能设置域为 *
res.header("Access-Control-Allow-Origin", req.headers.origin);
// 允许的header类型
res.header("Access-Control-Allow-Headers", "content-type");
// 跨域允许的请求方式
res.header("Access-Control-Allow-Methods", "DELETE,PUT,POST,GET,OPTIONS");
// 要设置允许客户端携带验证信息
res.header("Access-Control-Allow-Credentials", true);
console.log("cookie信息");
console.log(req.cookies);
res.end("");
});

/**
* 开启监听
*/
app.listen(port, function () {
console.log("ok");
});

注:

  • 同源 ajax 请求是可以自动携带 cookie 的
  • 而非同源需要客户端和服务端都做处理:
    • 客户端需要对 xhr 对象设置 withCredentials:true
    • 服务端需要设置响应头 access-control-allow-credentials:true
      • 同时必须指明 access-control-allow-origin 为服务方的 origin, 不能为*

新增属性:
Chrome 51 开始,浏览器的 Cookie 新增加了一个SameSite属性,用来防止 CSRF 攻击和用户追踪。
可选值:

  • Strict
    Strict最为严格,完全禁止第三方 Cookie,跨站点时,任何情况下都不会发送 Cookie。换言之,只有当前网页的 URL 与请求目标一致,才会带上 Cookie。
  • Lax
    Lax规则稍稍放宽,大多数情况也是不发送第三方 Cookie,但是导航到目标网址的 Get 请求除外。
  • None
    None相当于忽略该属性。不过,前提是必须同时设置 Secure 属性(Cookie 只能通过 HTTPS 协议发送),否则无效。

相关东西,后面补充。

小结:
跨域数据存储访问推荐做法:

  • 把文件放在一起或者使用 Nginx 代理到一起 (推荐)
  • 如果使用的域名可以考虑设置document.domain属性
  • 完全不同源,可以考虑通过跨域通信的方式传递相关参数

跨域脚本 API 访问(Cross-origin script API access)

如果两个网页不同源,就无法拿到对方的 DOM。典型的例子是iframe窗口和window.open方法打开的窗口,它们与父窗口无法通信。

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

1
2
document.getElementById("myIFrame").contentWindow.document;
// Uncaught DOMException: Blocked a frame with origin "http://127.0.0.1:5500" from accessing a cross-origin frame.

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

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

1
2
window.parent.document.body;
// Uncaught DOMException: Blocked a frame with origin "http://localhost:1000" from accessing a cross-origin frame.

对于完全不同源的网站,目前有四种方法,可以解决跨域窗口的通信问题。

  • 片段标识符(fragment identifier
  • window.name
  • 跨文档通信 API(Cross-document messaging
  • WebSocket

片段标识符

片段标识符(fragment identifier)指的是,URL 的#号后面的部分,比如http://example.com/x.html#fragment#fragment如果只是改变片段标识符,页面不会重新刷新。也可以叫做锚点, 用作页面定位.

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

1
2
3
4
5
var srcStr =
document.getElementsByClassName("iframe")[0].src.split("#")[0] +
"#" +
encodeURI(value);
$(".iframe").attr("src", srcStr);

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

1
2
3
4
5
6
window.onhashchange = checkMessage;

function checkMessage() {
var message = window.location.hash;
// ...
}

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

1
parent.location.href = target + "#" + hash;

注意:如果修改后的 hash 值和原来的一样,不会进片段标识符改变的监听

window.name

浏览器窗口有window.name属性。这个属性的最大特点是,无论是否同源,只要在同一个窗口里,前一个网页设置了这个属性,后一个网页可以读取它。

父窗口先打开一个子窗口,载入一个不同源的网页,该网页将信息写入 window.name 属性。

1
window.name = '{"name": "lisi"}';

接着,子窗口跳回一个与主窗口同域的网址。

1
2
// 把子页面的window.location 设置为域和父页面同源的
window.location = "http://127.0.0.1:5500/windowName/testWN.html";

然后,主窗口就可以读取子窗口的 window.name 了。

1
var data = document.getElementById("myFrame").contentWindow.name;

这种方法的优点是,window.name容量很大,可以放置非常长的字符串;
代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var state = 0;
(iframe = document.createElement("iframe")),
(loadfn = function () {
if (state === 1) {
var data = iframe.contentWindow.name; // 读取数据
alert(data); // 你好,我是子页面的window.name, 携带了一些数据
} else if (state === 0) {
state = 1;
iframe.contentWindow.location =
"http://127.0.0.1:10229/page/windowName/null.html"; // 设置的代理文件
}
});
iframe.src = "http://localhost:1000/windowname-nginx.html";
if (iframe.attachEvent) {
iframe.attachEvent("onload", loadfn);
} else {
iframe.onload = loadfn;
}
document.body.appendChild(iframe);

window.postMessage

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

这个 API 为 window 对象新增了一个方法 window.postMessage,允许跨窗口通信,不论这两个窗口是否同源,只要你能获取到目标对象的 window 对象。

举例来说,父窗口http://aaa.com向子窗口http://bbb.com发消息,调用postMessage方法就可以了。

1
2
var popup = window.open("http://aaa.com", "title");
popup.postMessage("Hello World!", "http://aaa.com");

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

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

1
window.opener.postMessage("Nice to see you", "http://bbb.com");

注:window.opener 属性是一个可读可写的属性,可返回对创建该窗口的 Window 对象的引用

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

1
2
3
4
5
6
7
window.addEventListener(
"message",
function (e) {
console.log(e.data);
},
false
);

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

1
2
3
4
window.addEventListener("message", receiveMessage);
function receiveMessage(event) {
event.source.postMessage("Nice to see you!", "*");
}

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

1
2
3
4
5
6
7
8
9
window.addEventListener("message", receiveMessage);
function receiveMessage(event) {
if (event.origin !== "http://bbb.com") return;
if (event.data === "Hello World") {
event.source.postMessage("Hello", event.origin);
} else {
console.log(event.data);
}
}

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

  • event.source:发送消息的窗口
  • event.origin: 消息发向的网址
  • event.data: 消息内容

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

1
2
3
4
window.addEventListener("message", receiveMessage);
function receiveMessage(event) {
event.source.postMessage("Nice to see you!", "*");
}

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

1
2
3
4
5
6
7
8
9
window.addEventListener("message", receiveMessage);
function receiveMessage(event) {
if (event.origin !== "http://bbb.com") return;
if (event.data === "Hello World") {
event.source.postMessage("Hello", event.origin);
} else {
console.log(event.data);
}
}

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

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

1
2
3
4
5
6
7
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

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

1
2
3
4
5
6
var win = document.getElementsByTagName("iframe")[0].contentWindow;
var obj = { name: "Jack" };
win.postMessage(
JSON.stringify({ key: "storage", data: obj }),
"http://bbb.com"
);

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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;
}
};

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;
// "Jack"
console.log(JSON.parse(e.data).name);
};

SessionStorage 同理。

WebSocket

WebSocket 是一种通信协议,使用ws://(非加密)wss://(加密)作为协议前缀。该协议不实行同源策略,只要服务器支持,就可以通过它进行跨源通信。

下面是一个例子,浏览器发出的 WebSocket 请求的头信息(摘自维基百科)。

1
2
3
4
5
6
7
8
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

上面代码中,有一个字段是Origin,表示该请求的请求源(origin),即发自哪个域名。

正是因为有了Origin这个字段,所以 WebSocket 才没有实行同源策略。因为服务器可以根据这个字段,判断是否许可本次通信。如果该域名在白名单内,服务器就会做出如下回应。

1
2
3
4
5
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat

示例:

  • Node 服务端:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
const WebSocket = require("ws");

// websocket服务的端口
const wsport = 3006;
var wss = new WebSocket.Server({
port: wsport,
path: "/ws",
});
let webSocketServer = wss.on("connection", function connection(ws) {
console.log("ws连接");
ws.isAlive = true;
ws.on("pong", heartbeat);
ws.on("message", function incoming(message) {
message = JSON.parse(message);
sendAllMessage(webSocketServer, message);
});
});

// 广播消息
function sendAllMessage(server, message) {
server.clients.forEach((ws) => {
ws.send(JSON.stringify(message));
});
}
// 定时每30s发送ping监测连接是否中断
const interval = setInterval(function ping() {
wss.clients.forEach(function each(ws) {
if (ws.isAlive === false) return ws.terminate();
ws.isAlive = false;
ws.ping();
});
}, 30000);
  • 页面 1:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
var ws = new WebSocket("ws://localhost:3006/ws", "suyp");
ws.onopen = function (param) {
console.log("WebSocket连接成功!");
ws.onmessage = function (param) {
console.log("这是WebSocket接收的消息 --- " + param.data);
// 接收消息这里推荐写try catch 来捕捉错误,因为消息有可能不是JSON格式
try {
var message = JSON.parse(param.data);
} catch (e) {
console.log(e);
}
if (message) {
switch (message.key) {
case "closeInfoWindow":
console.log("弹窗已关闭");
break;
default:
break;
}
}
};
ws.onclose = function () {
console.log("ws关闭了");
};
};
  • 页面 2
1
2
3
4
5
6
7
8
9
10
11
// 向父页面发送消息,来关闭弹窗
function closeInfowWindow2() {
if (ws) {
ws.send(
JSON.stringify({
key: "closeInfoWindow",
msg: "",
})
);
}
}

小结
跨域脚本 API 访问推荐做法:

  • Nginx 代理到一起 (最好)
  • 使用 postMessaget 通信 (挺好)

参考资料

作者

苏永蓬

发布于

2020-08-27

更新于

2023-08-05

许可协议

CC BY-NC-SA 4.0

评论