跨域问题
在几次的面试中,都被问到前端的跨域方案问题。在实际的项目中,也总是遇到。这篇文章是在看那些年,那些跨域问题 后的笔记。
在这篇文章中理解什么是跨域 JavaScript跨域(1):什么是跨域,如何跨域
具体策略限制情况可看下表
URL | 说明 | 允许通信 |
http://www.a.com/a.js http://www.a.com/b.js | 同一域名下 | 允许 |
http://www.a.com/lab/a.js http://www.a.com/script/b.js | 同一域名下不同文件夹 | 允许 |
http://www.a.com:8000/a.js http://www.a.com/b.js | 同一域名,不同端口 | 不允许 |
http://www.a.com/a.js https://www.a.com/b.js | 同一域名,不同协议 | 不允许 |
http://www.a.com/a.js http://127.0.0.100/b.js | 域名和域名对应ip | 不允许 |
http://www.a.com/a.js http://script.a.com/b.js | 主域相同,子域不同 | 不允许 |
http://www.a.com/a.js http://a.com/b.js | 同一域名,不同二级域名(同上) | 不允许 |
http://www.a.com/a.js http://www.b.com/b.js | 不同域名 | 不允许 |
前端跨域
- JSONP
- document.domain
- window.name
- window.postMessage
JSONP
JSONP 简介
原理
利用<script>
标签可以可以访问别的域名下的文件,并执行。
代码:在www.a.com
中请求www.b.com
的数据
script = document.createElement('script');
script.type = 'text/javascript';
script.src = 'http://www.b.com/getdata?callback=demo';
定义jsonp的回调函数 与 callback参数定义的value同名称callback=demo
function demo(data){
//处理数据
console.log(data)
}
过程
- a浏览器:发出请求http://www.b.com/getdata?callback=demo
- b服务器,接到请求,JSONP技术中服务器会接受回调函数名作为请求参数
callback=demo
(这里是函数名是demo),把数据{data:‘message’} 包装好, 最终返回的是demo({data:‘message’})。 - a浏览器获得 b服务器返回的数据
demo({data:'message'})
,其作为JavaScript代码来执行。 - 只要在a的页面中 声明 demo 函数来处理数据就行。
jsonp的实现需要服务器配合:
服务端的实现(php):
//http://www.b.com/getdata
function getdata(){
$callback = $this->getValue('callback');
$data = ['a'=>1];
if ($callback) {
echo 'window.'.$callback.' && '.$callback.'(' . json_encode(['errcode' => 0,'data'=>$data]) . ')';
//得到 window.demo && demo({"errcode":0,"data":{'a':1}})
} else {
echo json_encode(['errcode' => 0]);
}
}
示例:
jQuery
在jQuery中 使用$.ajax()
方法时,将 dataType
设置为jsonp
即可。
代码如下:
$.ajax({
url: 'http://www.b.com/getData.do',
dataType: 'jsonp',
jsonp: "callback",
jsonpCallback: "dosomething"
success: function(cb) {
console.log(cb);
},
error: function(cb) {
console.log(cb);
}
});
在这里找到了一些解释
参数:
- dataType: ‘jsonp’,用于表示这是一个 JSONP 请求。
- jsonp: ‘callback’,用于告知服务器根据这个参数获取回调函数的名称,通常约定就叫 callback。
- jsonpCallback: ‘dosomething’,回调函数的名称,也是前面callback参数的值。
实际发送出来的完整请求长这样:http://www.b.com/getData.do?callback=dosomething&_=1471419449018
。后面的随机字符串是jQuery加上的。
在那些年,那些跨域问题 中说
不指定回调名,可省略callback参数,会由jQuery自动生成
如果没有指定 jsonCallback
如:
$.ajax({
url: 'http://www.b.com/getdata?callback=?', //不指定回调名,可省略callback参数,会由jQuery自动生成
dataType: 'jsonp',
success: function(data) {
console.log(data.msg);
}
});
如果请求后拼参数callback=?
,即没有指定回调函数名称, 则jQuery会动态创建一个值赋给这个?,服务端debug可以看到这个值,格式是这样的:
那么jQuery会创建一个值,会执行$.ajax
内部的success
方法或error
方法 。
要注意的点
它只支持GET请求,这是由于该技术本身的特性所决定的。
document.domain
document.domain:获取/设置当前文档的原始域部分, 用于同源策略.
如,来自www.a.com
想要获取document.a.com
中的数据。只要基础域名相同,便可以通过修改document.domain为
基础域名的方式来进行通信,但是需要注意的是协议和端口也必须相同。
//document.a.com
document.domain = "a.com";
//www.a.com
document.domain = "a.com"
var iframe = document.createElement('iframe');
iframe.src = 'http://document.a.com';
iframe.style.display = 'none';
document.body.appendChild(iframe);
iframe.onload = function() {
var targetDocument = iframe.contentDocument || iframe.contentWindow.document;
//可以操作targetDocument
}
window.name
原理
- window.name属性就可以被共享
- window.name这个全局属性是用来获取和设置窗口名称的
实践
在页面a.html
( 8080端口
)中 跨域拿b.html
(8081端口
)的数据
在b.html
中设置 window.name
的值。
<h1>这是页面B</h1>
<script>
var data = {
name:'ziazan',
age:'24'
}
window.name = JSON.stringify(data);//name属性只支持字符串,支持最大2MB的数据
document.write('data in pageB window.name='+ window.name)
</script>
<h1>这是页面A</h1>
<script type="text/javascript">
var iframe = document.createElement('iframe');
// iframe.style.display = 'none';
var isLoad = false
document.body.appendChild(iframe);
iframe.onload = function (){
if(isLoad){
var data = JSON.parse(iframe.contentWindow.name); //拿到name值
document.write(data);
}else{
iframe.contentWindow.location = 'http://localhost:8081/html/ziazan/b.html';
isLoad = true;
}
}
</script>
运行结果:
(注:iframe.contentDocument 与 iframe.contentWindow 的区别)
报错:
Uncaught DOMException: Blocked a frame with origin “http://localhost:8080” from accessing a cross-origin frame.
此方法失败,但是在同域下,做数据的传递还是可以的。
window.postMessage
window.postMessage(message,targetOrigin)
message: 要传递的对象,只支持字符串信息
targetOrigin:目标域,需要注意的是协议,端口和主机名必须与要发送的消息的窗口一致。如果不想限定域,可以使用通配符“*”,但是从安全上考虑,不推荐这样做。
示例
a.html
( 8080端口
)中 发送数据给b.html
(8081端口
);
<h1>这是页面A,发送消息</h1>
<button id="sendMsg">发送data</button>
<br>
<iframe id="iframe_b" src="http://localhost:8081/html/ziazan/b.html" frameborder="0"></iframe>
<script type="text/javascript">
var data = {
name:'ziazan',
age:'24'
}
var btn = document.getElementById('sendMsg');
var iframe_b = document.getElementById('iframe_b').contentWindow;
btn.addEventListener('click',function(){
iframe_b.postMessage(JSON.stringify(data),'http://localhost:8081/html/ziazan/b.html');
})
</script>
<h2>这是页面B,接受消息</h2>
<div id="msg"></div>
<script>
window.addEventListener('message',function(e){
document.getElementById('msg').innerHTML = e.data;
})
</script>
运行结果
疑问
现在是a.html
向 b.html
发送消息,如果是页面a.html
要获取b.html中
的数据呢? window.postMessage
是不是只能是父到子的过程?
Hash
利用hash的改变不会触发页面修改的原理。
//页面A
var B = document.getElementByTagName('iframe');
B.src = B.src + '#' + 'data';
//页面B
window.onhashchange = function(){
var data = window.loaction.hash
}
WebSocket
WebSocket 无同源限制
let ws = new WebSocket('wss://echo.websocket.org');
ws.onopen = function (evt) {
console.log('Connection open ...');
ws.send('Hello WebSockets');
}
ws.onmessage = function (evt) {
console.log('Received Message:' + evt.data);
}
ws.onclose = function (evt) {
console.log('Connection closed')
}
未完待续…
服务器跨域
1.CORS
1.允许来自任何域的访问
location / {
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
if ($request_method = 'OPTIONS') {
return 204;
}
}
2.设置资源可跨域访问
location ~* \.(eot|ttf|woff)$ {
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Headers X-Requested-With;
add_header Access-Control-Allow-Methods GET,POST,OPTIONS;
}
3.设置允许的域 (http://a.com
上配置 允许http://b.com
的访问)
location / {
#允许跨域访问的域名,可以是一个域的列表,也可以是通配符*
add_header 'Access-Control-Allow-Origin' 'http://b.com';
#是否允许请求带有验证信息
add_header 'Access-Control-Allow-Credentials' 'true';
#允许脚本访问的返回头
add_header 'Access-Control-Allow-Headers' 'Authorization,Content-Type,Accept,Origin,User-Agent,DNT,Cache-Control,X-Mx-ReqToken,X-Requested-With';
#允许使用的请求方法,以逗号隔开
add_header 'Access-Control-Allow-Methods' 'GET,POST,OPTIONS';
...
}
2.反向代理
非简单请求是那种对服务器有特殊要求的请求,比如请求方法是PUT或DELETE,或者Content-Type字段的类型是application/json。
非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight)。
如果在 www.a.com
的页面 需要请求 www.b.com/apis/postData
接口。
在这里POST
方式的Content-Type
如果是是application/json
的格式,算复杂请求(见简单请求和复杂请求),设置了CORS 也会有一个预请求。询问服务器是否支持此Content-Type
。 这样性能不高,为了加速访问,可以在使用反向代理的方式进行跨域。
server {
...
location ^~ /apis/ {
proxy_pass http://www.b.com/; #注意域名后有一个/
}
...
}