共计 14152 个字符,预计需要花费 36 分钟才能阅读完成。
文章列出解决方案以及对应的 demo,拒绝说概念,不在稀里糊涂。
什么情况出现跨域?
- 协议不同
- 域名不同
- 端口不同
跨域解决方案
1. 同一个主域下不同子域之间的跨域请求 – document.domain+iframe
同一个 origin 下,父页面可以通过 iframe.contentWindow 直接访问 iframe 的全局变量、DOM 树等,iframe 可以也通过 parent/top 对父页面做同样的事情。
domain.html
<body>
<iframe id="ifr" src="http://b.tblog.com:3004/domain2.html"></iframe>
<script>
document.domain = 'tblog.com';
function aa(str) {console.log(str);
}
window.onload = function () {document.querySelector('#ifr').contentWindow.bb('aaa');
}
</script>
domain2.html
<body>
2222222222
<script>
document.domain = 'tblog.com';
function bb(str) {console.log(str);
}
parent.aa('bbb');
</script>
</body>
完整 demo
2. 完全不同源 – postMessage
html5 新增 API,支持 IE8+。
otherWindow.postMessage(message, targetOrigin, [transfer]);
- otherWindow 其他窗口的一个引用,比如 iframe 的 contentWindow 属性、执行 window.open 返回的窗口对象、或者是命名过或数值索引的 window.frames。
- message 将要发送到其他 window 的数据
- targetOrigin 通过窗口的 origin 属性来指定哪些窗口能接收到消息事件,其值可以是字符串 ”“(表示无限制)或者一个 URI。如果你明确的知道消息应该发送到哪个窗口,那么请始终提供一个有确切值的 targetOrigin,而不是。不提供确切的目标将导致数据泄露到任何对数据感兴趣的恶意站点。
- transfer 可选 是一串和 message 同时传递的 Transferable 对象. 这些对象的所有权将被转移给消息的接收方,而发送一方将不再保有所有权。
传递过来的 message 的属性有:
- data 从其他 window 中传递过来的对象。
- origin 调用 postMessage 时消息发送方窗口的 origin . 这个字符串由 协议、“://“、域名、“: 端口号”拼接而成
- source 对发送消息的窗口对象的引用; 您可以使用此来在具有不同 origin 的两个窗口之间建立双向通信
下面 index.html 和 index2.html 通信
index.html
<body>
<input type="text" placeholder="http://b.tblog.com:3004/index2.html">
<iframe src="http://192.168.101.5: 3004/index2.html" frameborder="0"></iframe>
<script>
const input = document.querySelector('input');
input.addEventListener('input', function () {window.frames[0].postMessage(this.value, '*');
// window.frames[0].postMessage(this.value, 'http://192.168.101.5');
// window.frames[0].postMessage(this.value, 'http://192.168.101.5:3004');
});
// 接收消息
window.addEventListener('message', function (e) {
input.value = e.data;
console.log('父窗口', e.data);
console.log('父窗口', e.source);
console.log('父窗口', e.origin);
});
</script>
</body>
index2.html
<body>
子窗口
<input id="input" type="text" placeholder="http://a.tblog.com:3004/index.html">
<script>
const input = document.querySelector('#input');
input.addEventListener('input', function () {window.parent.postMessage(this.value, '*');
});
// 接收消息
window.addEventListener('message', function (e) {
input.value = e.data;
console.log('子窗口', e.data);
console.log('子窗口', e.source);
console.log('子窗口', e.origin);
});
</script>
</body>
完整 demo
3. 完全不同源 – location.hash+iframe
原理是利用 location.hash 来进行传值。改变 hash 并不会导致页面刷新,所以可以利用 hash 值来进行数据传递,当然数据容量是有限的。
例如:假设 a.tblog.com:3004 和 192.168.101.5:3004/index2.html
通信
原理:a.tblog.com:3004 中 index.html 以 iframe 将 192.168.101.5:3004/index2.html
页面引入,在 192.168.101.5:3004/index2.html
中插入新的 iframe, 此 iframe 引入的页面和 a.tblog.com:3004 同源,就可将 192.168.101.5:3004/index2.html
的 hash 数据传入 a.tblog.com:3004 页面的 hash 值中。parent.parent.location.hash = self.location.hash.substring(1);
a.tblog.com:3004/index.html
<script>
var ifr = document.createElement('iframe');
ifr.style.display = 'none';
ifr.src = 'http://192.168.101.5:3004/ index2.html#paramdo';
document.body.appendChild(ifr);
function checkHash() {
try {var data = location.hash ? location.hash.substring(1) : '';
if (console.log) {console.log('Now the data is' + data);
}
} catch (e) {};}
setInterval(checkHash, 2000);
</script>
192.168.101.5:3004/ index2.html
<body>
<script>
// 模拟一个简单的参数处理操作
switch (location.hash) {
case '#paramdo':
callBack();
break;
case '#paramset':
//do something……
break;
}
function callBack() {
try {parent.location.hash = 'somedata';} catch (e) {var ifrproxy = document.createElement('iframe');
ifrproxy.style.display = 'none';
ifrproxy.src = 'http://a.tblog.com:3004/index3.html#somedata'; // 注意该文件在 "a.com" 域下
document.body.appendChild(ifrproxy);
}
}
</script>
</body>
a.tblog.com:3004/index3.html
<body>
<script>
// 因为 parent.parent 和自身属于同一个域,所以可以改变其 location.hash 的值
parent.parent.location.hash = self.location.hash.substring(1);
</script>
</body>
完整 demo
4. window.name + iframe 跨域
window.name 获取 / 设置窗口的名称。
窗口的名字主要用于为超链接和表单设置目标(targets)。窗口不需要有名称。
window.name 属性可设置或者返回存放窗口名称的一个字符串, name 值在不同页面或者不同域下加载后依旧存在,没有修改就不会发生变化,并且可以存储非常长的 name(2MB)。
场景 1 – 同源
a.html
<body>
<script type="text/javascript">
const iframe = document.createElement('iframe');
iframe.src = 'http://a.tblog.com:3004/b.html';
iframe.style.display = 'none';
document.body.appendChild(iframe);
iframe.onload = function () {console.log(iframe.contentWindow.name)
};
</script>
</body>
b.html
<body>
<script>
window.name = '子页面的数据';
</script>
</body>
场景 2 – 不同源
利用 iframe 中 window.name 在不同页面或者不同域下加载后依旧存在的特性。a.tblog.com:3004/a.html
中通过 iframe 添加 192.168.0.103:3004/b.html
(数据页面,指定 window.name 的值),监听 iframe 的 load,改变 iframe 的 src 与a.tblog.com:3004/a.html
同源代理页面a.tblog.com:3004/c.html
(空页面)。
a.tblog.com:3004/a.html
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
let state = 0;
iframe.onload = function () {console.log('iframe.onload', state, iframe.contentWindow);
if (state === 1) {const data = JSON.parse(iframe.contentWindow.name);
console.log(data, state);
iframe.contentWindow.document.write('');
iframe.contentWindow.close();
document.body.removeChild(iframe);
} else if (state === 0) {
state = 1;
console.log('数据', window.name)
iframe.contentWindow.location = 'http://a.tblog.com:3004/c.html';
}
};
iframe.src = 'http://192.168.0.103:3004/b.html';
document.body.appendChild(iframe);
完整 demo
5. 跨域 jsonp
jsonp 原理:
- 首先是利用 script 标签的 src 属性来实现跨域。
- 客户端注册 callback 方法名,携带在 URL 上,如 ’http://127.0.0.1:8080/getNews?callback=getData’
- 服务器响应后生成 json,将 json 放在刚才接收到的 callback 的函数中,就生成一段 getData(json)
- 客户端浏览器将 script 标签插入 DOM,解析 script 标签后,会执行 getData(json)。
由于使用 script 标签的 src 属性,因此只支持 get 方法
客户端代码
<body>
<button class="get">get data</button>
<script>
const btn = document.querySelector('.get');
btn.addEventListener('click', function () {const script = document.createElement('script');
script.setAttribute('src', 'http://127.0.0.1:8080/getNews?callback=getData');
document.head.appendChild(script);
document.head.removeChild(script);
})
function getData(news) {console.log(news)
}
</script>
</body>
服务端代码
const http = require('http');
const fs = require('fs');
const path = require('path');
const url = require('url');
http.createServer(function(req, res){const pathObj = url.parse(req.url, true);
switch(pathObj.pathname){
case '/getNews':
const news = [{id: 678}];
res.setHeader('Content-type', 'text/json; charset=utf-8');
if(pathObj.query.callback){res.end(pathObj.query.callback + '(' + JSON.stringify(news) + ')');
}else {res.end(JSON.stringify(news));
}
break;
default:
res.writeHead(404, 'not found');
}
}).listen(8080);
完整 demo
6. CORS 跨域
原理
跨域资源共享 (CORS) 是一种机制,它使用额外的 HTTP 头来告诉浏览器 让运行在一个 origin (domain) 上的 Web 应用被准许访问来自不同源服务器上的指定的资源。跨域资源共享(CORS)机制允许 Web 应用服务器进行跨域访问控制,从而使跨域数据传输得以安全进行。
什么情况下需要 CORS
- 前文提到的由 XMLHttpRequest 或 Fetch 发起的跨域 HTTP 请求。
- Web 字体 (CSS 中通过 @font-face 使用跨域字体资源), 因此,网站就可以发布 TrueType 字体资源,并只允许已授权网站进行跨站调用。
- WebGL 贴图
- 使用 drawImage 将 Images/video 画面绘制到 canvas
- 样式表(使用 CSSOM)
功能概述
跨域资源共享标准新增了一组 HTTP 首部字段,允许服务器声明哪些源站通过浏览器有权限访问哪些资源。允许服务器声明哪些源站通过浏览器有权限访问哪些资源。对于 get 以外的请求,浏览器必须首先使用 OPTIONS 方法发起一个预检请求(preflight request),从而获知服务端是否允许该跨域请求。服务器确认允许之后,才发起实际的 HTTP 请求。真个过程浏览器自动完成,服务器会添加一些附加的头信息,因此,实现 CORS 通信的关键是服务器。只要服务器实现了 CORS 接口,就可以跨源通信。
简单请求
某些请求不会触发 CORS 预检请求。本文称这样的请求为“简单请求”,请注意,该术语并不属于 Fetch(其中定义了 CORS)规范。只要同时满足以下两大条件,就属于简单请求:
(1) 请求方法是以下三种方法之一:HEAD
GET
POST(2)HTTP 的头信息不超出以下几种字段:Accept
Accept-Language
Content-Language
Last-Event-ID
Content-Type:只限于三个值 application/x-www-form-urlencoded、multipart/form-data、text/plain
请求响应结果多出的字段:
Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: FooBar
Content-Type: text/html; charset=utf-8
- Access-Control-Allow-Origin
Access-Control-Allow-Origin: <origin> | *
; 该字段是必须的。它的值要么是请求时 Origin 字段的值,要么是一个 *,表示接受任意域名的请求, 有次响应头字段就可以跨域
- Access-Control-Allow-Credentials
Access-Control-Allow-Credentials: true
; 当浏览器的 credentials 设置为 true 时, 此响应头表示是否允许浏览器读取 response 的内容,返回 true 则可以,其他值均不可以,Credentials 可以是 cookies, authorization headers 或 TLS client certificates。
Access-Control-Allow-Credentials 头 工作中与 XMLHttpRequest.withCredentials 或 Fetch API 中的 Request() 构造器中的 credentials 选项结合使用。Credentials 必须在前后端都被配置(即 the Access-Control-Allow-Credentials header 和 XHR 或 Fetch request 中都要配置)才能使带 credentials 的 CORS 请求成功。如果 withCredentials 为 false,服务器同意发送 Cookie,浏览器也不会发送,或者,服务器要求设置 Cookie,浏览器也不会处理。
需要注意的是,如果要发送 Cookie,Access-Control-Allow-Origin 就不能设为星号,必须指定明确的、与请求网页一致的域名。同时,Cookie 依然遵循同源政策,只有用服务器域名设置的 Cookie 才会上传,其他域名的 Cookie 并不会上传,且(跨源)原网页代码中的 document.cookie 也无法读取服务器域名下的 Cookie。
// 允许 credentials:
Access-Control-Allow-Credentials: true
// 使用带 credentials 的 XHR:var xhr = new XMLHttpRequest();
xhr.open('GET', 'http://example.com/', true);
xhr.withCredentials = true;
xhr.send(null);
// 使用带 credentials 的 Fetch:fetch(url, {credentials: 'include'})
- Access-Control-Expose-Headers
在跨域访问时,XMLHttpRequest 对象的 getResponseHeader()方法只能拿到一些最基本的响应头,Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma, 如果想拿到其他字段,就必须在 Access-Control-Expose-Headers 里面指定。上面的例子指定,getResponseHeader(‘FooBar’)可以返回 FooBar 字段的值。
代码如下:
<!-- 服务端 -->
http.createServer(function(req, res){const pathObj = url.parse(req.url, true);
switch(pathObj.pathname){
case '/user':
const news = [{id: 678}];
res.setHeader('Content-type', 'text/json; charset=utf-8');
res.setHeader('Access-Control-Allow-Origin', req.headers.origin);
// res.setHeader('Access-Control-Allow-Origin', '*');
// 需要 cookie 等凭证是必须
res.setHeader('Access-Control-Allow-Credentials', true);
res.end(JSON.stringify(news));
break;
default:
res.writeHead(404, 'not found');
}
}).listen(8080, (err) => {if (!err) {console.log('8080 已启动');
}
});
<!-- 客户端 -->
<body>
<script>
const xhr = new XMLHttpRequest();
xhr.open('GET', 'http://localhost:8080/user', true);
// 需要 cookie 等凭证是必须
xhr.withCredentials = true;
xhr.onreadystatechange = (e) => {console.log('onreadystatechange', e)
}
xhr.send();
</script>
</body>
完整 demo
非简单请求
非简单请求的 CORS 请求,会在正式通信之前,增加一次 HTTP 查询请求,称为 ” 预检 ” 请求(preflight)。
以获知服务器是否允许该实际请求。” 预检请求“的使用,可以避免跨域请求对服务器的用户数据产生未预期的影响。
当请求满足下述任一条件时,即应首先发送预检请求:
-
使用了下面任一 HTTP 方法:
- put
- delete
- connect
- OPTIONS
- trace
- patch
-
人为设置了对 cors 安全首部字段集合外的其他首部字段,该集合为:
- Accept
- Accept-Language
- Content-Language
- Content-Type
- DPR
- Downlink
- Save-data
- Viewport-Width
- Width
-
Content-Type 的值不属于下列之一:
- application/x-www-form-urlencoded
- multipart/form-data
- text/plain
- 请求中的 XMLHttpRequestUpload 对象注册了任意多个事件监听器。
- 请求中使用了 ReadableStream 对象。
如下是一个需要执行预检请求的 HTTP 请求:
<body>
<script>
const invocation = new XMLHttpRequest();
const url = 'http://localhost:8080/user';
const body = JSON.stringify({name: 'toringo'});
function callOtherDomain() {if (invocation) {invocation.open('POST', url, true);
invocation.setRequestHeader('X-PINGOTHER', 'pingpong');
invocation.setRequestHeader('Content-Type', 'application/json');
invocation.onreadystatechange = (e) => {console.log('onreadystatechange', e)
};
invocation.send(body);
}
}
callOtherDomain();
</script>
</body>
<!-- 服务端 -->
http.createServer(function(req, res){const pathObj = url.parse(req.url, true);
switch(pathObj.pathname){
case '/user':
const news = {id: 678};
res.setHeader('Content-type', 'text/json; charset=utf-8');
res.setHeader('Access-Control-Allow-Origin', req.headers.origin);
// res.setHeader('Access-Control-Allow-Origin', '*');
// 需要 cookie 等凭证是必须
res.setHeader('Access-Control-Allow-Credentials', true);
res.end(JSON.stringify(news));
break;
default:
res.writeHead(404, 'not found');
}
}).listen(8080, (err) => {if (!err) {console.log('8080 已启动');
}
});
浏览器请求结果:
cors2.html:1 Access to XMLHttpRequest at 'http://localhost:8080/user' from origin 'http://127.0.0.1:3004' has been blocked by CORS policy: Request header field x-pingother is not allowed by Access-Control-Allow-Headers in preflight response.
如图所示发起了预检请求,请求头部多了两个字段:
Access-Control-Request-Method: POST; // 该字段是必须的,用来列出浏览器的 CORS 请求会用到哪些 HTTP 方法.
Access-Control-Request-Headers: Content-Type, X-PINGOTHER; 告知服务器,实际请求将携带两个自定义请求首部字段:X-PINGOTHER 与 Content-Type。服务器据此决定,该实际请求是否被允许。
上例需要成功响应数据,服务端需要同意:
http.createServer(function(req, res){const pathObj = url.parse(req.url, true);
switch(pathObj.pathname){
case '/user':
const news = {id: 678};
res.setHeader('Content-type', 'text/json; charset=utf-8');
res.setHeader('Access-Control-Allow-Origin', req.headers.origin);
// 新增的
res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'X-PINGOTHER, Content-Type');
res.setHeader('Access-Control-Max-Age', 86400);
res.end(JSON.stringify(news));
break;
default:
res.writeHead(404, 'not found');
}
}).listen(8080, (err) => {if (!err) {console.log('8080 已启动');
}
});
服务段新增的字段:
Access-Control-Allow-Origin: req.headers.origin
Access-Control-Allow-Methods: POST, GET, OPTIONS // 表明服务器允许客户端使用 POST, GET 和 OPTIONS 方法发起请求。该字段与 HTTP/1.1 Allow: response header 类似,但仅限于在需要访问控制的场景中使用。这是为了避免多次 "预检" 请求。Access-Control-Allow-Headers: X-PINGOTHER, Content-Type // 如果浏览器请求包括 Access-Control-Request-Headers 字段,则 Access-Control-Allow-Headers 字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在 "预检" 中请求的字段。Access-Control-Max-Age: 86400 // 表明该响应的有效时间为 86400 秒,也就是 24 小时。在有效时间内,浏览器无须为同一请求再次发起预检请求。请注意,浏览器自身维护了一个最大有效时间,如果该首部字段的值超过了最大有效时间,将不会生效。
7. nodejs 代理跨域
node 中间件实现跨域代理,是通过一个代理服务器,实现数据的转发,也可以通过设置 cookieDomainRewrite 参数修改响应头中 cookie 中域名,实现当前域的 cookie 写入,方便接口登陆认证。
原理:服务器之间数据请求不存在跨域限制(同源策略是浏览器行为),所以先将请求代理到代理服务器,代理服务器在内部请求真实的服务器得到结果后 end 连接。
<!-- 服务 -->
http.createServer(function(req, res){const pathObj = url.parse(req.url, true);
console.log('server', pathObj.pathname)
switch(pathObj.pathname){
case '/user':
const news = {id: 678};
res.end(JSON.stringify(news));
break;
default:
res.setHeader('Content-type', 'text/json; charset=utf-8');
res.end('未知错误');
}
}).listen(4000, (err) => {if (!err) {console.log('4000 已启动');
}
});
<!-- 代理 -->
http.createServer(function(req, res){const pathObj = url.parse(req.url, true);
switch(pathObj.pathname){
case '/user':
res.setHeader('Content-type', 'text/json; charset=utf-8');
res.writeHead(200, {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'POST, GET, OPTIONS',
'Access-Control-Allow-Headers': 'X-PINGOTHER, Content-Type',
});
console.log('proxy', req.method, pathObj.pathname);
// 请求真实服务器
const proxyRequest = http.request({
host: '127.0.0.1',
port: 4000,
url: '/',
path: pathObj.pathname,
method: req.method,
headers: req.headers
}, (proxyRes) => {
let body = '';
proxyRes.on('data', (chunk) => {body += chunk;});
proxyRes.on('end', () => {console.log('响应的数据' + body);
res.end(body);
})
}).end();
break;
default:
res.writeHead(404, 'not found');
res.end(body);
break;
}
}).listen(8080, (err) => {if (!err) {console.log('8080 已启动');
}
});
<!-- 客户端 index.html -->
<body>
<script>
const invocation = new XMLHttpRequest();
const url = 'http://localhost:8080/user';
const body = JSON.stringify({name: 'toringo'});
function callOtherDomain() {if (invocation) {invocation.open('POST', url, true);
// invocation.setRequestHeader('X-PINGOTHER', 'pingpong');
invocation.setRequestHeader('Content-Type', 'application/json');
invocation.onreadystatechange = (e) => {console.log('onreadystatechange', e)
};
invocation.send(body);
}
}
callOtherDomain();
</script>
</body>
注意:
服务器和浏览器数据交互也需要遵循同源策略
— 持续更新 —
Tips:
代码地址。~ github
WeChat
参考文章
https://developer.mozilla.org…
http://vinc.top/2017/02/09/%E…
http://www.ruanyifeng.com/blo…
https://segmentfault.com/a/11…
https://developer.mozilla.org…
https://developer.mozilla.org…
http://www.ruanyifeng.com/blo…