同源策略限制以下几种行为:
- 1.Cookie、LocalStorage 和 IndexDB 无法读取
- 2.DOM 和 Js对象无法获得
- 3.AJAX 请求不能发送
//服务端代码
const Koa = require('koa');
const app = new Koa();
app.use(async ctx => {
ctx.body = 'Hello World';
});
app.listen(3000);
//浏览器端代码
let xhr = new XMLHttpRequest()
xhr.open("GET", "http://localhost:3000/api")
xhr.onreadystatechange = function(response) {
console.log(response)
}
xhr.send()
同源策略
MDN 中的定义,同源策略是一个重要的安全策略,它用于限制一个origin的文档或者它加载的脚本如何能与另一个源的资源进行交互。它能帮助阻隔恶意文档,减少可能被攻击的媒介。通俗的说 浏览器 端不可以直接访问同源网站。而同源呢指的是 域名、 协议、 端口 都相同。
域跨起来
今天主要总结一下跨域的方法:
- 1.jsonp
- 2.cors
- 3.代理
- 4.domain + iframe
- 5.location.hash + iframe跨域
- 6.window.name + iframe跨域
- 7.postMessage跨域
1.jsonp
jsonp 是利用 script 标签具有跨域请求资源的能力。
- 浏览器端生成 script 标签资源 src 指向目标接口。
- 服务端将响应数据包装成一个 JS function,并调用 function 将响应数据回调出去。
// 客户端代码
let url = "http://localhost:3000/api";
function cb(res){
console.log(res) //响应数据
}
function jsonp(url){
let script = document.createElement("script");
script.src = url;
document.body.appendChild(script)
}
jsonp(url)
//服务端代码
app.use(async ctx => {
ctx.body = `function fn(cb){ cb("Hello World") } fn(cb)`;
});
由于 jsonp 是依赖标签加载资源的能力在老式浏览器有着较好的兼容性,浏览器端没有办法设置 请求头、请求方法 ... ,所以项目完全用 jsonp 来实现跨域不可能的。
2.CORS ( Cross-origin resource sharing )
CORS接收到此次请求后 , 首先会判断Origin是否在允许源(由服务端决定)范围之内,如果验证通过,服务端会在Response Header 添加 Access-Control-Allow-Origin、Access-Control-Allow-Credentials等字段。
//服务端代码
const Koa = require('koa');
const app = new Koa();
app.use(async ctx => {
ctx.body = 'Hello World'
ctx.set("Access-Control-Allow-Origin","http://localhost:8000")
});
app.listen(3000);
3.代理
既然跨域是由于浏览器的同源策略影响,那么浏览器可以请求当前域的服务通过代理去访问服务端接口,问题迎刃而解。
//浏览器端,当前域名http://localhost:8000/api
let xhr = new XMLHttpRequest()
xhr.open("GET", "http://localhost:8000/api")
xhr.onreadystatechange = function(response) {
console.log(response)
}
xhr.send()
//前端静态资源服务
const Koa = require('koa');
const fs = require("fs");
const path = require("path");
const http = require("http");
const app = new Koa();
app.use(async ctx => {
let apiReg = /^\/api/;
if( apiReg.test( ctx.request.url ) ){
let text = await new Promise((resolve, reject) => {
http.get({
hostname: 'localhost',
port: 3000,
path: ctx.request.url,
}, (res) => {
let buf = [];
res.on('data', (data)=> {
buf.push(data)
})
res.on('end', () => {
let text = buf.toString();
resolve(text)
})
});
})
ctx.body = text;
}else{
let html = fs.readFileSync( path.resolve(__dirname, "./index.html"),{encoding: 'utf8' } )
ctx.body = html;
}
});
app.listen(8000);
4.domain + iframe
此方案仅限主域相同,子域不同的跨域应用场景。
实现原理:两个页面都通过js强制设置document.domain为基础主域,就实现了同域。
1.父窗口:(www.domain.com/a.html)
<iframe id="iframe" src="http://child.domain.com/b.html"></iframe>
<script>
document.domain = 'domain.com';
var user = 'admin';
</script>
2.子窗口:(child.domain.com/b.html)
<script>
document.domain = 'domain.com';
// 获取父窗口中变量
alert('get js data from parent ---> ' + window.parent.user);
</script>
5.location.hash + iframe跨域
实现原理: a欲与b跨域相互通信,通过中间页c来实现。 三个页面,不同域之间利用iframe的location.hash传值,相同域之间直接js访问来通信。
具体实现:A域:a.html -> B域:b.html -> A域:c.html,a与b不同域只能通过hash值单向通信,b与c也不同域也只能单向通信,但c与a同域,所以c可通过parent.parent访问a页面所有对象。 1.a.html:(www.domain1.com/a.html)
<iframe id="iframe" src="http://www.domain2.com/b.html" style="display:none;"></iframe>
<script>
var iframe = document.getElementById('iframe');
// 向b.html传hash值
setTimeout(function() {
iframe.src = iframe.src + '#user=admin';
}, 1000);
// 开放给同域c.html的回调方法
function onCallback(res) {
alert('data from c.html ---> ' + res);
}
</script>
2.b.html:(www.domain2.com/b.html)
<iframe id="iframe" src="http://www.domain1.com/c.html" style="display:none;"></iframe>
<script>
var iframe = document.getElementById('iframe');
// 监听a.html传来的hash值,再传给c.html
window.onhashchange = function () {
iframe.src = iframe.src + location.hash;
};
</script>
3.c.html:(www.domain1.com/c.html)
<script>
// 监听b.html传来的hash值
window.onhashchange = function () {
// 再通过操作同域a.html的js回调,将结果传回
window.parent.parent.onCallback('hello: ' + location.hash.replace('#user=', ''));
};
</script>
6.window.name + iframe跨域
ifreame 的 window.name属性的独特之处:name值在不同的页面(甚至不同域名)加载后依旧存在,并且可以支持非常长的 name 值(2MB)。
- a.html:(www.domain1.com/a.html)
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);
});
- proxy.html:(www.domain1.com/proxy....
中间代理页,与a.html同域,内容为空即可。 - b.html:(www.domain2.com/b.html)
<script>
window.name = 'This is domain2 data!';
</script>
总结:通过iframe的src属性由外域转向本地域,跨域数据即由iframe的window.name从外域传递到本地域。这个就巧妙地绕过了浏览器的跨域访问限制,但同时它又是安全操作。
7.postMessage
postMessage是HTML5 XMLHttpRequest Level 2中的API,且是为数不多可以跨域操作的window属性之一,它可用于解决以下方面的问题:
- 页面和其打开的新窗口的数据传递
- 多窗口之间消息传递
- 页面与嵌套的iframe消息传递
- 上面三个场景的跨域数据传递
<iframe id="iframe" src="http://www.domain2.com/b.html" style="display:none;"></iframe>
<script>
var iframe = document.getElementById('iframe');
iframe.onload = function() {
var data = {
name: 'aym'
};
// 向domain2传送跨域数据
iframe.contentWindow.postMessage(JSON.stringify(data), 'http://www.domain2.com');
};
// 接受domain2返回数据
window.addEventListener('message', function(e) {
alert('data from domain2 ---> ' + e.data);
}, false);
</script>
b.html:(www.domain2.com/b.html)
<script>
// 接收domain1的数据
window.addEventListener('message', function(e) {
alert('data from domain1 ---> ' + e.data);
var data = JSON.parse(e.data);
if (data) {
data.number = 16;
// 处理后再发回domain1
window.parent.postMessage(JSON.stringify(data), 'http://www.domain1.com');
}
}, false);
</script>
参考
- 前端常见跨域解决方案(全) --安静de沉淀