我所了解的跨域

什么是跨域

由于浏览器的同源策略限制,导致了我们无法跨域获取数据。呸,是获取数据之后浏览器出于安全考虑将信息拦截。

那么什么是同源策略?

同源策略限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的重要安全机制。

同源策略的用处

接口请求

由于服务端Server会给登陆过的你发送一个cookie用以身份验证,以保证你一段时间内不需要每次都进行重复的登陆。可是若无同源策略的限制,那么当你打开其他网站时,他们亦可以通过你的主机向给你发送cookie的网站(咱们假装他叫A)服务端发送请求。

之后如你所想,他们也会得到一个cookie。这东西相当于是别人拿到了你的身份证一样而你却完全不知情。如此一来,若是A网站是一些敏感的信息网站甚至是银行的网站,那么发生什么事情咱都不敢想。

这是典型的CSRF攻击。

CSRFCross Site Request Forgy——跨站请求伪造。

在其他网站对目标网站的请求是在管理员不知情的情况下完成的

所以在同源策略限制的条件之下,我们至少可以防止一些低端的CSRF,不至于让cookie这么轻易的暴露在其他不明网站的攻击下

DOM查询
1
2
3
4
5
6
7
// HTML
<iframe name="yinhang" src="www.yinhang.com"></iframe>
// JS
// 由于没有同源策略的限制,钓鱼网站可以直接拿到别的网站的Dom
const iframe = window.frames['yinhang']
const node = iframe.document.getElementById('你输入账号密码的Input')
console.log(`拿到了这个${node},我还拿不到你刚刚输入的账号密码吗`)

若无同源策略限制,那么攻击者可以轻易的获取其他网站的DOM,并将其存至自己的iframe之中掩人耳目,借此盗取你的敏感信息。

怎么着才算跨域?

域名的组成

同源策略是指“协议”、“域名”、“端口”三者相同。即使两个不同的域名指向同一个ip地址亦非同源

咱们可以通过这张图做个例子

在这里插入图片描述

这些跨域咱也没办法

  1. 协议和端口不同那咱们再咋操作也没办法

  2. 即使ip相同,你端口、协议和域名不匹配照样不行

那咋办

JSONP

由于<script>标签不受跨域限制,所以我们可以得到从其他来源动态产生的JSON数据。

但是!!!!!JSONP请求一定需要对方的服务器支持!!!!!!

实现
  • 声明一个回调函数,其函数名(如show)当做参数值,要传递给跨域请求数据的服务器,函数形参为要获取目标数据(服务器返回的data)。

  • 创建一个<script>标签,把那个跨域的API数据接口地址,赋值给script的src,还要在这个地址中向服务器传递该函数名(可以通过问号传参:?callback=show)。

  • 服务器接收到请求后,需要进行特殊的处理:把传递进来的函数名和它需要给你的数据拼接成一个字符串,例如:传递进去的函数名是show,它准备好的数据是show('我不爱你')

  • 最后服务器把准备的数据通过HTTP协议返回给客户端,客户端再调用执行之前声明的回调函数(show),对返回的数据进行操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Client
function jsonp({ url, params, callback }) {
return new Promise((resolve, reject) => {
let script = document.createElement('script')
window[callback] = function(data) {
resolve(data)
document.body.removeChild(script)
}
params = { ...params, callback } // wd=b&callback=show
let arrs = []
for (let key in params) {
arrs.push(`${key}=${params[key]}`)
}
script.src = `${url}?${arrs.join('&')}`
document.body.appendChild(script)
})
}
jsonp({
url: 'http://localhost:3000/say',
params: { wd: 'Iloveyou' },
callback: 'show'
}).then(data => {
console.log(data)
})
1
2
3
4
5
6
7
8
9
10
// Server
let express = require('express')
let app = express()
app.get('/say', function(req, res) {
let { wd, callback } = req.query
console.log(wd) // Iloveyou
console.log(callback) // show
res.end(`${callback}('我不爱你')`)
})
app.listen(3000)
优点

兼容性好,可解决主流浏览器的跨域数据访问

缺点

只支持get方式。可能会受到XSS攻击

CORS

实现CORS的重点在于后端,而前端则像往常一样发送数据即可。但是发送时会出现两种情况:简单请求&复杂请求

简单请求

同时满足以下两大条件即是简单请求

条件1:使用方法为GETPOSTHEAD之一

条件2:Content-Type值为text/plainmultipart/form-data或`application/x-www-form-urlencoded`

复杂请求

不符合简单请求的就是复杂请求啦

复杂请求的CORS会在正是通信之前增加一次HTTP查询请求。称为预检。该请求通过option方法来打听服务端是否允许跨域请求

Node中间代理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var express = require('express');
var request = require('request');
var app = express();

app.use(express.static('./public'))

app.use('/', function(req, res) {
var url = 'http://192.168.1.111:8080' + req.url;
req.pipe(request(url)).pipe(res);
process.env.PORT = process.env.PORT || 8080;
});
app.listen(process.env.PORT || 8080, () => {
console.log("开始监听" + "......");
});

如此就可以完成啦。作为一个中间件转发请求node代理成功地避开了浏览器和服务器之间交互的冲突,将锅甩给了可以无障碍通信的服务器。我们只需要访问本地端口就可以啦。

nginx反向代理

使用nginx反向代理实现跨域,是最简单的跨域方式。只需要修改nginx的配置即可解决跨域问题,支持所有浏览器,支持session,不需要修改任何代码,并且不会影响服务器性能。

实现思路:通过nginx配置一个代理服务器(域名与domain1相同,端口不同)做跳板机,反向代理访问domain2接口,并且可以顺便修改cookiedomain信息,方便当前域cookie写入,实现跨域登录。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// nginx/nginx.conf
server {
listen 81;
server_name www.domain1.com;
location / {
proxy_pass http://www.domain2.com:8080; #反向代理
proxy_cookie_domain www.domain2.com www.domain1.com; #修改cookie里域名
index index.html index.htm;

# 当用webpack-dev-server等中间件代理接口访问nignx时,此时无浏览器参与,故没有同源限制,下面的跨域配置可不启用
add_header Access-Control-Allow-Origin http://www.domain1.com; #当前端只跨域不带cookie时,可为*
add_header Access-Control-Allow-Credentials true;
}
}

最后通过命令行nginx -s reload启动nginx

websocket

通过这个咱们可以实现服务端和客户端的双向通信,真正的平等对话。

1
2
3
4
5
6
7
8
9
// 前端页面
let socket = new WebSocket('ws://localhost:3000');

socket.onopen = function () {
socket.send('你好');
}
socket.onmessage = function (e) {
console.log(e.data);
}
1
2
3
4
5
6
7
8
9
10
11
// node后端
let express = require('express');
let app = express();
let WebSocket = require('ws');
let wss = new WebSocket.Server({port:3000});
wss.on('connection',function(ws) {
ws.on('message', function (data) {
console.log(data);
ws.send('你好')
});
})

postMessage

window.postMessage()是HTML5的一个接口,专注实现不同窗口不同页面的跨域通讯。

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
<template>
<div>
<button @click="postMessage">给http://crossDomain.com:9099发消息</button>
<iframe name="crossDomainIframe" src="http://crossdomain.com:9099"></iframe>
</div>
</template>

<script>
export default {
mounted () {
window.addEventListener('message', (e) => {
// 这里一定要对来源做校验
if (e.origin === 'http://crossdomain.com:9099') {
// 来自http://crossdomain.com:9099的结果回复
console.log(e.data)
}
})
},
methods: {
// 向http://crossdomain.com:9099发消息
postMessage () {
const iframe = window.frames['crossDomainIframe']
iframe.postMessage('我是[http://localhost:9099], 麻烦你查一下你那边有没有id为app的Dom', 'http://crossdomain.com:9099')
}
}
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<template>
<div>
我是http://crossdomain.com:9099
</div>
</template>

<script>
export default {
mounted () {
window.addEventListener('message', (e) => {
// 这里一定要对来源做校验
if (e.origin === 'http://localhost:9099') {
// http://localhost:9099发来的信息
console.log(e.data)
// e.source可以是回信的对象,其实就是http://localhost:9099窗口对象(window)的引用
// e.origin可以作为targetOrigin
e.source.postMessage(`我是[http://crossdomain.com:9099],我知道了兄弟,这就是你想知道的结果:${document.getElementById('app') ? '有id为app的Dom' : '没有id为app的Dom'}`, e.origin);
}
})
}
}
</script>

document.domain

这个咱们得让两个页面同属于一个域下。

1
2
127.0.0.1 a.fullstackjavascript.cn
127.0.0.1 b.fullstackjavascript.cn
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// a.html
<iframe src="http://b.fullstackjavascript.cn:4000/b.html"
frameborder="0" id="frame"
onload="load()"
></iframe>
<script>
function load() {
document.domain = 'fullstackjavascript.cn';
console.log(frame.contentWindow.name);
}
</script>

// b.html
<script>
document.domain = 'fullstackjavascript.cn';
var name = 'jw';
</script>

访问http://a.fullstackjavascript.cn:3000/a.html发现是可以拿到另一个页面中的值

location.hash

通过三方域完成

三个页面hash1,hash2,hash3。hash1和hash2是同域下,hash3在独立域下。

我们通过hash1页面用iframe引入hash3页面,可以在引入时给hash3页面传递hash值,hash3接到hash值后算出需要返回结果,在创建iframe引入hash2把结果通过hash的方式传递给hash2hash2hash1是同域的,hash2可以直接操控hash1的值,此时hash1页面可以监控hash值的变化来获取hash2的数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// hash1.html
<iframe src="http://a.fullstackjavascript.cn:3000/hash3.html#iloveyou" frameborder="0"></iframe>
<script>
window.onhashchange = function () {
console.log(location.hash);
}
</script>

// hash2.html
<script>
let hash = location.hash;
let data;
if(hash === '#iloveyoue'){
data = 'idontloveyou'
}
let frame = document.createElement('iframe');
frame.src = `http://a.fullstackjavascript.cn:3000/hash3.html#${data}`;
</script>

// hash3.html
<script>
let hash = location.hash;
window.parent.parent.location.hash = hash
</script>

window.name

还是有三个页面a,b,c。a和b是同域下的,c自己一个域。a先引用c,c将想表达的内容放到name,属性上之后,a改变引用路径,改成引用b,此时name属性不会被删除,因为a,b是同域的,所以可以直接获取。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// a.html
<iframe src="http://a.fullstackjavascript.cn:4000/c.html" id="myFrame" onload="load()" frameborder="0"></iframe>
<script>
let first = true
function load() {
if(first){
myFrame.src = 'http://b.fullstackjavascript.cn:3000/c.html';
first = false
}else{
let name = myFrame.contentWindow.name;
console.log(name);
}
}
</script>
// c.html
<script>
window.name = '我爱你'
</script>