关于跨域及跨域的解决方式

关于跨域及跨域的解决方式

参考原文:

https://segmentfault.com/a/1190000015597029

https://www.cnblogs.com/chenshishuo/p/4919224.html

前言

博主正在写毕设,刚刚搭建完服务器的环境,由于用的是前后端分离的方式写网站,然后就遇到了不可避免的跨域的问题,然后在网上找了点资料,加之自己试了试,然后把相关内容在这里记录一下,避免以后遇到同样的问题,希望同时也能帮到大家~~

目录

关于跨域

跨域是什么?同源策略是什么?

首先不废话,对于同源策略先总结一句:一个域分为 协议、地址、端口 三个部分,其任一不同,即为不同的域。

同源策略一般会针对两个方面:①接口请求 ②Dom查询。

不同域之间请求数据或对象等资源,即为跨域,如以下例:

https://macrazds.cn:8080/a.html
请求地址 协议 域名/地址 端口 路径 文件 请求是否跨域
http://macrazds.cn:8080/a.html
https://macrazdhao.cn:8080/a.html
https://macrazds.cn:3000/a.html
https://macrazds.cn:8080/test/a.html
https://macrazds.cn:8080/b.html

为什么会出现跨域?

总的来说,就一句话,是浏览器对于Javascript的同源策略在搞鬼,不同域之间的Js无法对互相共享对象和资源等等。具体的官方解释:

https://developer.mozilla.org/zh-CN/docs/Web/Security/Same-origin_policy

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

反正就是一个浏览器的安全机制,于是就出现了让我们这么蛋疼的问题。

没有同源策略会发生什么?

因为同源策略是根据接口请求、Dom查询这两个方面来的,所以这里针对这两个场景做分析:

  1. 接口请求

首先说一说Cookie,一般用作网站的登陆场景,使服务端知道发送请求的是哪个用户。当你登陆了某个网站,并通过验证,服务端就会在响应头添加 Set-Cookie 字段,然后当你对该服务端发送其他请求时,浏览器就会把 cookie 加在HTTP请求头字段Cookie中,服务端接收到该请求后,就能知道发送该请求打的是哪个用户,并且已经登录过了。

接下来分析一下针对接口请求,没有同源策略的情境下,会发生什么危险的事情。

首先打个比方,你在某宝登录正在挑选商品,与此同时,某个求求好友给你发来了个不知道什么网址,在他的各种威逼利诱之下,你点开了这个迷之网址,于是故事就发生了。

当你好心好意地看着这个迷之网址的时候,然这个迷之网址却在背地里做着一些不可理喻的勾当!!而此时正因为你的浏览器没有同源策略!!这个迷之网址竟然向某宝发送了附带 Set-Cookie 的HTTP请求!!接下来就是上演为所欲为二人组表情包的时刻了!

试想一下,如果你登陆的是某银行,后果不堪设想。

上面说的,其实就是 CSRF攻击 ,详细可以看这篇文章:

http://www.cnblogs.com/hyddd/archive/2009/04/09/1432744.html

然而后来我了解到,cookie是明文的,即使有同源策略也一样不安全。于是后来又发现又httpOnly这玩意儿,可以使js不能获取cookie的内容。如果没有httpOnly,通过XSS就可以获取到Cookie。然后就是通过Secure,可以保证在https的加密通信中传输以防截获。

另外附上:

XSS攻击相关文章:

https://www.cnblogs.com/TankXiao/archive/2012/03/21/2337194.html

Cookie/Session的机制与安全

https://harttle.land/2015/08/10/cookie-session.html

对于这一篇文章,这里我浅谈一下这两点收获:

  1. Cookie防篡改机制是怎么防篡改的?

情景:首先用户发送请求到服务端,服务端在返回相应数据的时候。

  • 服务端由一个不为人知的字符串( 称为Secret ),然后根据Secret对所需要返回的值 somevalue=123 ,与改Secret进行计算生成 签名 6hTiBl7lVpd1P 。

  • 于是返回 Set-Cookie: somevalue=123|6hTiBl7lVpd1P 。

  • 在用户收到该相应的应答后,打算 篡改 该数据的值,如篡改为 somevalue=9999 ,并再次发送请求。

  • 由于用户不知道这个仅存于服务端的Secret字符串,无法生成对应签名,只能随意填写成 Set-Cookie: somevalue=99999|??? 。

  • 服务端收到该请求后,对该Cookie进行签名校验,便会发现重新生成的签名,与发过来的签名(???)不一致。

  1. Session是如何避免客户端Cookie存储敏感数据的?

首先理解一下SessionId的来历,这里以将Session存储在redis为例(Session 可以存储在HTTP服务器的内存中,也可以存在内存数据库(如redis)中, 对于重量级的应用甚至可以存储在数据库中):

  • 当用户通过了登录请求时,服务端会将该用户名存储在redis中,并生成对应在redis中的ID,该ID即为SessionID。

  • 通过SessionID,可以从redis中取出用户对象(该用户对象存储着敏感数据)。

那么Session是如何避免客户端Cookie存储敏感数据的呢?

这里分为两种情景来讨论,没有Session,及有Session的情景:

  • 没有Session:当用户需要发送以某个敏感信息为基础的请求(请求1)前,需要先请求该敏感信息,并存储到Cookie,才能继续发送那个请求1。此时Cookie存储为 somevalue=123 。

  • 有Session:当用户需要发送以某个敏感信息为基础的请求前,无需请求该敏感信息,只需要设置Cookie为 sessionId=xxxxxx|somevalue 。

综上对比,由于Cookie是明文的,可以明显看得出哪个更加安全。

  1. Dom查询

简单来说就是钓鱼网站,这种钓鱼网站不是通过伪造页面来实现的,而已通过iframe。即比如正常的网站地址的是macrazds.cn,然而你访问的是macrazbs.cn,此时该网站通过iframe指定src,显示的是macrazds.cn的内容。

当你登陆时,会发生什么呢?

当你登陆的时候,这个macrazbs.cn就可以通过js来获取你在iframe中登陆时输入账号密码的input的内容了,示例代码如下:

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

综上,通过接口请求、Dom查询这两点,可以清晰地了解到有了同源策略可以安全很多(并不是说绝对安全)。

实现跨域

在这里,基于同源策略下,我会对接口请求、Dom查询这两个方向分别来给出具体的实现方案。

接口请求

JSONP

参考(推荐):

https://www.cnblogs.com/giggle/p/5496596.html

本质上来讲,jsonp不过是通过一个script标签实现的跨域请求。也正因如此,所以只能实现GET请求。

实现原理:

  • 凡是拥有src属性的标签都拥有跨域的能力,比如script、img及iframe。

  • 通过上一条,可以很直接地想到,可以利用script标签,将需要返回的数据通过js文件返回,从而获取到数据,然后供用户进一步处理。

  • 原生js支持JSON格式的数据,因此,根据上一条,我们可以在服务端将数据以JSO你的数据格式存储在js文件当中,以实现返回数据。

  • 根据上一点,加之用户所需要的数据不是固定一成不变的,因此需要动态生成JSON文件,以便动态地将数据填充,然后返回给用户。

  • 为了便于前端使用数据,这逐渐形成了一种非正式的传输协议,人们将其称为JSONP,该协议允许用户传递一个函数作为参数给服务端(callback回调函数),当服务端返回数据时,就会将返回的数据以参数的形式传递给callback函数,再通过callback函数进行处理。

代码实现:

后端(Node.js + Express):

首先在根目录的routes文件夹下新建一个jsonp.js,代码如下:

1
2
3
4
5
6
7
8
var express = require('express');
var router = express.Router();
router.get('/', function(req, res, next) {
console.log("收到来自jsonp的请求,内容是");
console.log(res);
res.jsonp({msg: "this is jsonp!!"});
});
module.exports = router;

在根目录下的app.js添加以下代码:

1
2
var jsonpRouter = require('./routes/jsonp');
app.use('/jsonp', jsonpRouter);

这样,后端的代码就完成了。

接下来看下前端发送请求的代码是如何实现的:

  • 原生js实现:

在body中添加以下:

1
2
3
4
5
6
<script type="text/javascript">
function test(data){
alert(data.msg);
}
</script>
<script src="http://localhost:3000/jsonp?callback=test"></script>

当看到以下弹窗的时候,即表示成功。

jsonp请求结果

  • Vue.js实现:

首先我们安装一个依赖包vue-jsonp

1
$ npm install vue-jsonp --save

打开vue项目,src目录中的main.js,添加以下两行代码:

1
2
import VueJsonp from 'vue-jsonp'
Vue.use(VueJsonp);

接着在相对应的页面,及需要发送jsonp请求的函数中添加:

1
2
3
4
5
6
7
this.$jsonp('http://localhost:3000/jsonp',{msg: "this is a jsonp request!!"}).then(json=>{
console.log('这是jsonp返回的信息:');
console.log(json);
}).catch(err=>{
console.log('出错了!!呜呜呜!!');
console.log(err);
});

接着在控制台看到相应输出,即表示成功。

空iframe加form

由于JSONP仅限于GET请求,那如果要发送POST请求的时候,该如何办呢?

这时候可以通过利用空iframe加form的方法来替代,具体如何实现请继续往下看~

注意:该方法因为是基于form表单的,所以也仅限于GET和POST请求。

代码实现:

后端(Node.js + Express):

首先在根目录的routes文件夹下新建一个jsonp.js,代码如下:

1
2
3
4
5
6
7
8
9
10
var express = require('express');
var router = express.Router();
/* POST users listing. */
router.post('/', function(req, res, next) {
console.log("收到来自iframe的form请求,内容是");
console.log(res);
res.set('Content-Type', 'text/html');
res.send("<p name='resinfo'>i got the post request!!</p>");
});
module.exports = router;

这里需要注意的是,Content-Type需要设置为text/html,否则在前端会出现MIME的警告:

Resource interpreted as Document but transferred with MIME type application/json

在根目录下的app.js添加以下代码:

1
2
var jsonpRouter = require('./routes/iframe');
app.use('/iframe', iframeRouter);

这样,后端的代码就完成了。

接下来看下前端发送请求的代码是如何实现的:

接着在相对应的页面,及需要发送jsonp请求的函数中添加:

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
const requestPost = ({ url, data }) => {
// 首先创建一个用来发送数据的iframe.
const iframe = document.createElement("iframe");
iframe.name = "iframePost";
//这里一般会通过以下一条命令隐藏iframe,但这里为了显示效果,所以这里就注释掉这一句不隐藏了
//iframe.style.display = "none";
document.body.appendChild(iframe);
const form = document.createElement("form");
const node = document.createElement("input");
// 注册iframe的load事件处理程序,如果你需要在响应返回时执行一些操作的话.
iframe.addEventListener("load", function(res) {
console.log("post success!!");
// 取出返回的信息
console.log(iframe.contentWindow.document.getElementsByName('resinfo')[0].innerHTML);
});
// 指定请求url
form.action = url;
// 在指定的iframe中执行form
form.target = iframe.name;
form.method = "post";
// 将data中的数据作为新建input赋名称和值,并拼接在form内(逐行数据对应逐行创建input)
for (let name in data) {
node.name = name;
node.value = data[name].toString();
form.appendChild(node.cloneNode());
}
// 表单元素需要添加到主文档中
form.style.display = "none";
document.body.appendChild(form);
form.submit();
// 表单提交后,就可以删除这个表单,不影响下次的数据发送.
document.body.removeChild(form);
};
// 使用方式
requestPost({
url: "http://localhost:3000/iframe",
data: {
msg: "helloIframePost"
}
});

接着看页面及控制台的提示信息,即可知道请求是否发送成功。

CORS

CORS是一个W3C标准,全称”跨域资源共享”(Cross-origin resource sharing)。

这里引用并推荐一下阮一峰老师的文章:

http://www.ruanyifeng.com/blog/2016/04/cors.html

浏览器将CORS请求划分为:简单请求(simple request)和非简单请求(not-so-simple request)。

  1. 简单请求:
  • 定义:

满足一下两大条件,即为简单请求:

(1) 请求方法是以下三种方法之一:

  • HEAD

  • GET

  • POST

(2)HTTP的头信息不超出以下几种字段:

  • Accept

  • Accept-Language

  • Content-Language

  • Last-Event-ID

  • Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain

凡是不同时满足上面两个条件,就属于非简单请求。

浏览器对这两种请求的处理,是不一样的。

  • 流程:

对于简单请求,浏览器会直接发出CROS请求。具体的说,就是在头部增加一个 Origin 字段,其内容是本次请求的来源(包括协议、域名、端口),服务端会据此决定是否统一这次请求。

如果Origin指定的域名在许可范围内,服务器返回的响应,会多出以下几个头信息字段:

1
2
3
4
Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: FooBar
Content-Type: text/html; charset=utf-8

以上三个 Access-Control- 开头的字段的含义分别为:

(1)Access-Control-Allow-Origin
该字段是必须的。它的值要么是请求时Origin字段的值,要么是一个*,表示接受任意域名的请求。

(2)Access-Control-Allow-Credentials
该字段可选。它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为true,如果服务器不要浏览器发送Cookie,删除该字段即可。

(3)Access-Control-Expose-Headers
该字段可选。CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。上面的例子指定,getResponseHeader(‘FooBar’)可以返回FooBar字段的值。

另外,需要注意的是,如果需要把Cookie发送到服务器,除了服务端设置Access-Control-Allow-Credentials为true之外,还需要在前端发送请求时,给AJAX请求设置withCredentials属性为true,如:

1
2
var xhr = new XMLHttpRequest();
xhr.withCredentials = true;

否则无论如何,浏览器也不会发送Cookie。

但是,如果省略withCredentials设置,有的浏览器还是会一起发送Cookie。这时,可以显式关闭withCredentials:

1
xhr.withCredentials = false;

需要注意的是:

  • 如果要发送Cookie,Access-Control-Allow-Origin就不能设为星号,必须指定明确的、与请求网页一致的域名。同时,Cookie依然遵循同源政策,只有用服务器域名设置的Cookie才会上传,其他域名的Cookie并不会上传,且(跨源)原网页代码中的document.cookie也无法读取服务器域名下的Cookie。

  • 不要给浏览器本身设置跨域,否则会导致跨域测试出现问题,比如发送的请求包中的header中没有origin。

  • withCredential、crossDomain不是header的内容。另外在axios中设置这两者,以下:

1
2
axios.defaults.withCredentials=true;
axios.defaults.crossDomain=true;
  • Access-Control-Expose-Headers、Access-Control-Allow-Methods允许为’*’,表示任意适配。

  • Cookie是浏览器从服务端发到浏览器的应答包的header中所包含的set-cookie中得来,然后浏览器再根据其域名存储,不同域名/地址之间无法共享。如,macrazds.cn的cookie只能发送给macrazds.cn,而不能发给macrazdhao.cn。

代理

这一方法的思路是:浏览器将请求发送到正常的域名,然后有某个处于前后端两者中间东西,负责转发,将浏览器发来的请求发送到真正的后端域名,于是这样就避免了跨域。

emmm。。还需要注意的是,这个方法我没试验过,因为服务器没装Nginx,所以感觉有点麻烦,所以这里就直接搬运了…

Nginx配置:

1
2
3
4
5
6
7
8
9
10
server{
# 监听9099端口
listen 9099;
# 域名是localhost
server_name localhost;
#凡是localhost:9099/api这个样子的,都转发到真正的服务端地址http://localhost:9871
location ^~ /api {
proxy_pass http://localhost:9871;
}
}

前端只需要写接口就好了:

1
2
3
4
5
6
7
8
9
10
11
// 请求的时候直接用回前端这边的域名http://localhost:9099,这就不会跨域,然后Nginx监听到凡是localhost:9099/api这个样子的,都转发到真正的服务端地址http://localhost:9871 
fetch('http://localhost:9099/api/iframePost', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
msg: 'helloIframePost'
})
})

来自参考原文的评价:

Nginx转发的方式似乎很方便!但这种使用也是看场景的,如果后端接口是一个公共的API,比如一些公共服务获取天气什么的,前端调用的时候总不能让运维去配置一下Nginx,如果兼容性没问题(IE 10或者以上),CROS才是更通用的做法吧。

Dom查询

postMessage

window.postMessage()这个方法是HTML5新增的自带接口,专注于实现不同窗口/页面的跨域通讯。以下示例:

发送方http://localhost:9099/#/crossDomain:

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>

接收方http://crossdomain.com:9099:

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

该方法仅适用于同主域名,而不同子域名的iframe跨域。

方法的实现:

如主域名是http://crossdomain.com:9099,子域名是http://child.crossdomain.com:9099,这种情况下给两个页面指定一下document.domain即document.domain = crossdomain.com就可以访问各自的window对象了。

canvas操作图片的跨域问题

具体参考:

https://www.zhangxinxu.com/wordpress/2018/02/crossorigin-canvas-getimagedata-cors/