跨域
浏览器同源策略
同源:如果两个页面的协议,端口和域名都相同,则两个页面同源
同源策略限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互,这是一个用于隔离潜在恶意文件的重要安全机制
跨域方法
jsonp
以淘宝网搜索框为例,输入电脑
可以看到页面向后端发送了 get 请求,其 url 为
1 | https://suggest.taobao.com/sug?code=utf-8&q=%E7%94%B5%E8%84%91&_ksTS=1572254071001_546&callback=jsonp547&k=1&area=c2c&bucketid=16 |
其中有参数为callback=jsonp547
,在浏览器输入该 url,返回
1 | jsonp547({"result":[["电脑椅","112957.25494307223"],["电脑包","185667.87352325322"],["电脑台式桌","53369.62559995722"],["电脑主机","127618.426922485"],["电脑笔记本","192247.33896850277"],["电脑包女双肩","225481.05965832053"],["电脑音响","31156.14154566319"],["电脑椅家用","62166.67675737684"],["电脑家用桌","190343.38565178483"],["电脑包15.6寸","16574.673889620495"]],"magic":[{"index":"2","type":"tag_group","data":[[{"title":"17寸"},{"title":"15寸"},{"title":"14寸","type":"hot"}],[{"title":"Asus\/华硕","type":"hot"},{"title":"Dell\/戴尔"}],[{"title":"双肩","type":"hot"},{"title":"单肩包"},{"title":"女士"},{"title":"笔记本"},{"title":"商务"}]]},{"index":"5","type":"tag_group","data":[[{"title":"苹果"},{"title":"三星"},{"title":"四核"},{"title":"戴尔"},{"title":"游戏本"},{"title":"独显"},{"title":"分期","type":"hot"},{"title":"14英寸"},{"title":"酷睿"},{"title":"华硕"}]]},{"index":"7","type":"tag_group","data":[[{"title":"USB供电","type":"hot"},{"title":"usb供电"}],[{"title":"Hivi\/惠威"},{"title":"炫目"},{"title":"低音炮"},{"title":"播放器","type":"hot"},{"title":"组合"},{"title":"漫步者"},{"title":"台式"},{"title":"笔记本"}]]}]}) |
可以看到,返回的值是一个名为jsonp547
的函数,其参数为我们需要获取的数据,如果将callback
的名字改成a
,则返回
1 | a({"result":[["电脑椅","112957.25494307223"],["电脑包","185667.87352325322"],["电脑台式桌","53369.62559995722"],["电脑主机","127618.426922485"],["电脑笔记本","192247.33896850277"],["电脑包女双肩","225481.05965832053"],["电脑音响","31156.14154566319"],["电脑椅家用","62166.67675737684"],["电脑家用桌","190343.38565178483"],["电脑包15.6寸","16574.673889620495"]],"magic":[{"index":"2","type":"tag_group","data":[[{"title":"17寸"},{"title":"15寸"},{"title":"14寸","type":"hot"}],[{"title":"Asus\/华硕","type":"hot"},{"title":"Dell\/戴尔"}],[{"title":"双肩","type":"hot"},{"title":"单肩包"},{"title":"女士"},{"title":"笔记本"},{"title":"商务"}]]},{"index":"5","type":"tag_group","data":[[{"title":"苹果"},{"title":"三星"},{"title":"四核"},{"title":"戴尔"},{"title":"游戏本"},{"title":"独显"},{"title":"分期","type":"hot"},{"title":"14英寸"},{"title":"酷睿"},{"title":"华硕"}]]},{"index":"7","type":"tag_group","data":[[{"title":"USB供电","type":"hot"},{"title":"usb供电"}],[{"title":"Hivi\/惠威"},{"title":"炫目"},{"title":"低音炮"},{"title":"播放器","type":"hot"},{"title":"组合"},{"title":"漫步者"},{"title":"台式"},{"title":"笔记本"}]]}]}) |
可见,返回的函数名与我们传入的参数值有关,根据这个,我们想要跨域,可以通过创建一个<script>
标签,并将其 src 设置为对应的 url 来获取数据,然后在 html 中运行返回的函数
1 | function jsonp({ url, params, callback }) { |
服务端的代码为
1 | const express = require('express') |
运行后可以看到
1 | I love you |
1 | I love you too |
jsonp 的缺点是只能发送 get 请求,而且不安全,易导致 xss 攻击
CORS
假设有这样一种情况,有一个页面 index.html,要通过 http://localhost:3000 来访问它,对应的后端代码可以设置为
1 | const express = require('express') |
在页面中如果想获取数据,可以设置 ajax
1 | <script> |
然而如果想获取的数据来自其他域( http://localhost:5000/say )下,则会报错
1 | Access to XMLHttpRequest at 'http://localhost:5000/say' from origin 'http://localhost:3000' has been blocked by CORS policy: |
很明显,由于端口号不同,该请求被浏览器同源策略所阻止,但是这个请求是被浏览器所屏蔽,接受端服务器依然可以收到请求,另一个服务器代码如下
1 | const express = require('express') |
可以看到,server2 的命令行输出了请求的头
1 | { host: 'localhost:5000', |
可以看到,其中有一个 header 描述了当前访问 server2 的源
1 | origin: 'http://localhost:3000' |
可以设置一个“白名单”,将允许访问的源添加进去,并添加响应头Access-Control-Allow-Origin
1 | // code ... |
设置完后在访问页面,可以看到数据成功获取
1 | I love you |
如果在页面中设置一个请求头
1 | <script> |
发现浏览器报错
1 | Access to XMLHttpRequest at 'http://localhost:5000/say' from origin 'http://localhost:3000' has been blocked by CORS policy: |
显然,需要添加响应头
1 | app.use((req, res, next) => { |
重启之后可以看到浏览器正常接收数据,server2 的命令行显示
1 | { host: 'localhost:5000', |
其中有
1 | name: 'tfcx' |
如果需要通过 ajax 发送其他类型的请求,比如 put
1 | <script> |
浏览器显示
1 | Access to XMLHttpRequest at 'http://localhost:5000/say' from origin 'http://localhost:3000' has been blocked by CORS policy: |
在服务端添加响应头Access-Control-Allow-Methods
1 | app.use((req, res, next) => { |
重启之后可以看到浏览器正常接收数据,打开浏览器控制台可以看到,浏览器在发送 PUT 请求之前先发送了一个 OPTIONS 请求,该请求称为预检请求,用于判断是否能跨域,可以设置对 OPTIONS 请求不做处理;同时可以看到,以不同的时间刷新页面时,有时会发送预检请求,而有时则不会,可已设置一个最大时间
1 | app.use((req, res, next) => { |
如果设置了 cookie
1 | <script> |
由于 cookie 不能跨域,打开页面会发现 server2 并没有收到 cookie,可以在页面中设置withCredentials
为 true
1 | <script> |
此时刷新页面可以看到浏览器报错
1 | Access to XMLHttpRequest at 'http://localhost:5000/say' from origin 'http://localhost:3000' has been blocked by CORS policy: |
可以看出还需要配置一个头
1 | app.use((req, res, next) => { |
重启服务器后可以看到,cookie 被正常发送
1 | { host: 'localhost:5000', |
如果想设置响应的头
1 | app.put('/say', (req, res) => { |
在页面中输出这个头的值
1 | <script> |
浏览器再次报错,并且输出的结果为 null
1 | Refused to get unsafe header "location" |
但是在浏览器的调试界面可以看到,这个响应头是被成功发送的,还需要进一步的配置使浏览器认为这个头是安全的
1 | app.use((req, res, next) => { |
postMessage
一个窗口可以获得对另一个窗口的引用,然后在窗口上调用targetWindow.postMessage()
方法分发一个 MessageEvent 消息,接收消息的窗口可以根据需要自由处理此事件
传递给window.postMessage()
的参数将通过消息事件对象暴露给接收消息的窗口
假设有两个页面 a.html 和 b.html,其对应的端口分别问 3000 和 5000
a.html 中包含一个 iframe ,引用 http://localhost:5000/b.html ,向 b.html 发送数据,同时接收其发回的数据
1 | <body> |
1 | <body> |
1 | I love you // => b.html |
window.name
window.name 是一个全局属性,只要在一个 window 下,无论 url 怎么变化,window.name 都不会改变
在 iframe 中,即使 src 在变化,iframe 中的 window.name 也是一个固定的值