跨域这块是前后端分离必经的一条路, 这次好好整理一下关于跨域方面知识.

什么是跨域?

跨域是受同源策略的影响而导致的, 指一个源下的资源试图操作另一个源下的资源.

那么, 什么又是同源策略呢?
就是限制从一个源的资源如何与另一个源的资源交互, 用于隔离潜在的恶意文件, 保证文件的安全性. 所谓的同源是指: 协议/域名/端口 三者相同, 即使两个不同的域名指向同一个 IP 地址, 也非同源. 如果没有同源策略, 浏览器很容易受到 XSS/CSRF 等的攻击(XSS: 跨站脚本攻击;CSRF: 跨站请求伪造).

同源策略将限制以下行为:

  • Cookie/LocalStorage/IndexDB
  • DOM 无法获得
  • Ajax 请求无法发送

跨域解决方案

1. JSONP

它的特性是简单/ 兼容性好/ 改造小, 但是不支持 POST 请求. 原理是通过 script 标签放入回调函数, 服务端将返回数据塞入回调函数即可.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const createScript = url => {
let script = document.createElement('script')
script.type = 'text/javascript'
script.src = url
document.body.appendChild(script)
}

// 传入请求地址
createScript('http://www.example.com/api?callback=foo')

// 回调函数
function foo (data) {
// 服务端返回数据
console.log(data)
}

2. document.domain

通过设置域名来访问 Cookie 和 操作 iframe 窗口, 此方案只适用于主域相同, 子域不同的场景.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 父窗口 (http://www.main.com/a.html)
<iframe id='iframe' src='http://www.child.main.com/b.html'></iframe>
<script>
document.domain = 'main.com'
document.cookie = 'test=hi'
var user = 'admin'
</script>


// 子窗口 (http://www.child.main.com/b.html)
<script>
document.domain = 'example.com'
let parentMessage = document.cookie
console.log(window.parent.user)
</script>

3. window.location.hash

通过修改 hash 值来传递参数, 修改 hash 值并不会刷新页面但字节数有限制.

1
2
3
4
5
6
7
8
9
10
11
// 父窗口向子窗口写入 hash
let src = `${childUrl}#test=hi`
document.querySelector('#iframe').src = src

// 子窗口
// 接收消息
window.onhashchange = () => {
let meg = window.location.hash
}
// 子窗口向父窗口写入 hash
window.parent.href = `${window.parent.href}#world`

4. window.name

window.name 只要在同一个窗口, 无论是否同源前一个页面设置了这个属性, 后一个页面就能读取. 它可以支持2M 大小的值但是变化需要自己监听.

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
// 父窗口 (http://www.main.com/a.html)
const proxy = (url, callback) => {
const iframe = document.createElement('iframe')
let state = 0

// 跳转到跨域页面
iframe.src = url

iframe.onload = () => {
if (state === 1) {
// 第二次代理也加载成功之后, 读取 name 属性
callback(window.name)
document.body.removeChild(iframe);
} else if (state === 0) {
// 第一次跳转到代理页面
iframe.src = 'http://www.main.com/proxy.html'
state = 1
}
}
document.body.appendChild(iframe)
}

// 传入跨域页面 URL
proxy('http://www.child.com/b.html', (name) => {
console.log(name) // 'This message from b.html'
})

// 跨域子窗口 (http://www.child.com/b.html)
// 设置 name 属性
window.name = 'This message from b.html'

5. postMessage

postMessage(跨文档通信) 是 HTML5 中为了解决跨域出的 API, 可以解决以下几种问题:

  1. 页面和其打开的新窗口的数据传递
  2. 多窗口之间消息传递
  3. 页面与嵌套的iframe消息传递
  4. 上面三个场景的跨域数据传递

发送消息通过 postMessage(data, origin) 方法, 该方法接收两个参数:

  • data: 需要传递的参数, 由于部分浏览器只支持字符串, 所以传递之前最好先 JSON.stringify() 序列化
  • origin: 是接收方的 协议 + 主机 + 端口, 可以是设置为 *, 指定与当前窗口同源的话设置为 /.

接收消息通过 message 事件监听, 该事件有一个 event 参数提供三个属性:

  • event.origin: 消息接收方
  • event.source: 消息来源
  • event.data: 消息体
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
// 父窗口 (http://www.main.com/a.html)
<firame id='iframe' src='http://www.child.com/b.html'></iframe>
<script>
let iframe = document.querySelector('#iframe')
let url = iframe.src
iframe.onload = () => {
let data = {
name: 'parent',
message: 'This message from parent'
}
// 跨域向子窗口发送消息
iframe.contentWindow.postMessage(JSON.stringify(data), url)
}

// 父窗口监听子窗口发送的消息
window.onmessage = (event) => {
// 过滤不属于自己的消息
if (data.origin !== 'http://www.main.com/a.html') return
console.log('data from child: ' + event.data)
}
</script>

// 子窗口 (http://www.child.com/b.html)
<script>
window.onmessage = (event) => {
console.log('data from main: ' + event.data)
let data = JSON.parse(event.data)
data.name = 'child'

// 收到消息后处理再发送给父窗口
window.parent.postMessage(JSON.stringify(data), 'http://www.main.com/a.html')
}
</script>

5. CORS

跨域资源共享(Cross-origin-resource-sharing), 它允许浏览器向跨源服务器发出 XMLHttpRequest 请求. CORS 现在也是主流的跨域解决方案, 这种方式只需要后端做处理, 如果要带上 cookie 那么前后端都要设置.

带 cookie 传输:

1
xhr.withCredentials = true

请求实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const XMLRequest = (method = 'GET', url = '', data = null) => {
let xhr = null
if (window.XMLHttpRequest) {
xhr = new XMLHttpRequest()
} else {
xhr = new ActiveXObject('Microsoft.XMLHTTP')
}

xhr.open(method, url, true)
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')
xhr.send(data)

xhr.onreadystatechange = () => {
if (xhr.readyState === 4 && xhr.status === 200) {
console.log(xhr.responseText)
}
}
}

XMLRequest('POST', 'http://www.example.com/api', {
name: 'cara'
})

其中 readyState 有五种状态:
0: 为初始化
1: 服务器连接建立
2: 请求已接收
3: 请求处理中
4: 请求已完成

6. 代理服务器

其实就是通过配置 nginx 实现一个中间服务器作跳板, 代理到目标服务器.

静态资源代理

1
2
3
location / {
add_header Access-Control-Allow-Origin *;
}

具体代理配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
server {
listen 81;
server_name www.example.com;

lication / {
# 反向代理
proxy_pass http://www.example2.com:8080;
# 修改 cookie 域名
proxy_cookie_domain www.example2.com www.example.com;
index index.html index.htm;

# 不带 cookie 时才能设置为 *
add_header Access-Control-Allow-Origin http://www.example.com;
add_header Access-Control-Allow-Credentials true;
}
}

调用示例

1
2
3
4
5
let xhr = new XMLHttpRequest()

xhr.withCredentials = true
xhr.open('GET', 'htt://www.example.com:81/?user=admin', true)
xhr.send(null)

以上, 就是跨域导致的原因以及解决方案的大致总结.

Created on 2018-4-2 by Cara