前端所有跨域方式实现原理及详解

跨域是我们在项目中经常遇到的,前后端数据交互经常碰到请求跨域,首先我们来想一下为什么会有跨域这个词的出现?本文带你来探讨一下以下几个问题:

  • 跨域是什么?
  • 为什么要跨域?
  • 跨域的几种方式?

什么是跨域?

跨域是指的浏览器不能执行其它网站的脚本,它是由浏览器的同源策略造成,是浏览器对 JavaScript 实施的安全限制。

跨域实际上指从一个域的网页去请求另一个域的资源,比如:从 http://www.baidu.com 网站去请求http://www.google.com 网站的资源。

什么是同源策略?

同源策略 指的是 域名协议端口 三者都相同~

什么是同源?

要知道URL协议域名端口以及路径组成,若两个URL的协议、域名和端口相同,则表示他们同源。
相反,只要协议域名端口有任何一个的不同,就被当作是跨域。

限制同源策略内容

  • Cookie、LocalStorage、IndexedDB 等存储性内容
  • DOM 节点
  • Ajax 请求发送后,结果被浏览器拦截了

允许跨域加载资源

这下边三个含有 src 标签的是允许跨域加载资源的

1
<img src=XXX>
1
<link href=XXX>
1
<script src=XXX>

跨域的场景

九种跨域解决方案

  • jsonp
  • cors
  • postMessage
  • document.domain
  • window.name
  • location.hash
  • https-proxy
  • nginx
  • websocket

jsonp

什么是 jsonp

jsonp全称是JSON with Padding,是为了解决跨域请求资源而产生的解决方案,是一种依赖开发人员创造出的一种非官方跨域数据交互协议。

Jsonp 的原理

  1. 利用 script 标签的 src 属性来实现跨域
  2. 通过将前端方法作为参数传递到服务器端,然后由服务器端注入参数之后再返回,实现服务器端向客户端通信
  3. 由于使用 script 标签的 src 属性,因此只支持 get 方法

Jsonp 和 Ajax 对比

  1. Jsonp 和 Ajax 相同, 都是客户端向服务器端发送请求,从服务器端获取数据的方式
  2. Ajax 属于同源策略
  3. Jsonp 属于非同源策略 (跨域请求)

Jsonp 的优缺点

优点:

  1. 它不像 XMLHttpRequest 对象实现的 Ajax 请求那样受到同源策略的限制,JSONP 可以跨越同源策略
  2. 它的兼容性更好,在更加古老的浏览器中都可以运行,不需要 XMLHttpRequest 或 ActiveX 的支持
  3. 在请求完毕后可以通过调用 callback 的方式回传结果

缺点:

  1. 它只支持 GET 请求而不支持 POST 等其它类型的 HTTP 请求
  2. 它只支持跨域 HTTP 请求这种情况,不能解决不同域的两个页面之间如何进行 JavaScript 调用的问题
  3. jsonp 在调用失败的时候不会返回各种 HTTP 状态码
  4. 缺点是安全性,万一假如提供 jsonp 的服务存在页面注入漏洞,即它返回的 javascript 的内容被人控制的

Jsonp 的实现流程

  1. 声明一个回调函数,把函数名 (show) 当做参数值
  2. 要传递给跨域请求的数据的服务器,函数形参为要获取目标数据
  3. 创建一个 script 标签,把那个跨域的 API 数据接口地址,赋值给 script 的 src,还要在这个地址中向服务器传递该函数名
  4. 服务器接收到请求后,需要进行处理:把传递的参数名和它需要的数据拼接成一个字符串
  5. 最后服务器把准备的数据通过 HTTP 协议返回给客户端,客户端再调用执行之前声明的回调函数 (show), 对返回的数据进行操作

具体代码实现如下:

index.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
37
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta >
<title>Document</title>
</head>
<body>
<script>
function jsonp({url,params,cb}){
return new Promise((resolve,reject)=>{
let script = document.createElement('script');
window[cb]=function(data){
resolve(data);
document.body.removeChild(script);
}
params={...params,cb}
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:'早上好'},
cb:'show'
}).then(data=>{
console.log(data)
})
</script>
</body>
</html>

serve.js

1
2
3
4
5
6
7
8
9
let express = require('express');
let app = express();

app.get('/say',function (req,res){
let {wd,cb} = req.query;
console.log(wd);
res.end(`${cb}('晚上好')`)
})
app.listen(3000)

注意: 需要安装npm install express, 然后在终端里面输入node serve.js, 再把index.html在浏览器上边console栏查看返回结果

JQuery 的 jsonp 跨域请求

如果从 192.168.19.1ajax请求到 192.168.19.6 会产生跨域问题, 利用jqueryjsonp参数可轻松这个问题。

注意:Jsonp 都是 GET 和异步请求

1
2
3
4
5
6
7
8
9
10
11
function get() {
$.ajax({
type: "GET",
url: 'http://192.168.19.6:8080/jsgd/bill.jsp?userCode=?&date='+ new Date(),
dataType:"jsonp",
jsonp:"jsonpcallback",
success: function(msg){
$('#callcenter').html(msg.text);
}
});
}

cors

什么是 cors

cors全称 “跨域资源共享”(Cross-origin resource sharing), 是一种 ajax 跨域请求资源的方式。

兼容性

  1. cors 需要浏览器和服务器同时支持,才可以实现跨域的请求
  2. 这个方法几乎所有的浏览器都支持,但是 ie 必须是 10 以上
  3. ie8 和 9 需要通过 XDomainRequest 来实现

请求类型

cors 分为简单请求复杂请求两类

简单请求

请求方式使用下列方法之一:

1
2
3
GET
HEAD
POST

Content-Type 的值仅限于下列三者之一:

1
2
3
text/plain
multipart/form-data
application/x-www-form-urlencoded

注意:对于简单的请求,浏览器会直接发送 cors 请求,具体来说就是在 header 中加入 origin 请求头字段。在响应头回服务器设置相关的 cors 请求, 响应头字段为允许跨域请求的源。请求时浏览器在请求头的 Origin 中说明请求的源,服务器收到后发现允许该源跨域请求,则会成功返回。

复杂请求

使用了下面任一 HTTP 方法

1
2
3
4
5
6
PUT
DELETE
CONNECT
OPTIONS
TRACE
PATCH

Content-Type 的值不属于下列之一:

1
2
3
application/x-www-form-urlencoded
multipart/form-data
text/plain

当符合复杂请求的条件时,浏览器会自动先发送一个 options 请求。如果发现浏览器支持该请求,则会将真正的请求发送到后端。如果浏览器发现服务端不支持该请求,则会在控制台抛出错误。

cors 字段介绍

  • Access-Control-Allow-Methods

    这个字段是必要的,它的值是逗号分割的一个字符串,表明服务器支持的所有跨域请求的方式

  • Access-Control-Allow-Headers

    如果浏览器请求包括这个字段,则这个字段也是必须的,它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在 “预检 “ 中请求的字段

    • Access-Control-Allow-Credentials

      这个字段与简单请求时的含义相同

  • Access-Control-Max-Age

    该字段可选,用来指定本次预检请求的有效期,单位为秒。上面结果中,有效期是 20 天(1728000 秒),即允许缓存该条回应 1728000 秒(即 20 天),在此期间,不用发出另一条预检请求

流程实现

index.html

1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta >
<title>Document</title>
</head>
<body>
hello
</body>
</html>

serve.js

1
2
3
4
5
let express = require('express');
let app = express();
app.use(express.static(__dirname));

app.listen(3000)

以当前这个作为静态文件目录,先要在终端里面node serve.js服务器打开,访问localhost:3000就可以把 hello 显示出来。

这是一个完整的复杂请求例子:

index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let xhr = new XMLHttpRequest()
document.cookie = 'name=xiaoming'
xhr.withCredentials = true
xhr.open('PUT', 'http://localhost:4000/getData', true)
xhr.setRequestHeader('name', 'xiaoming')
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
console.log(xhr.response)
//得到响应头,后台需设置Access-Control-Expose-Headers
console.log(xhr.getResponseHeader('name'))
}
}
}
xhr.send()

serve.js

1
2
3
4
5
let express = require('express');
let app = express();
app.use(express.static(__dirname));

app.listen(3000)

serve2.js

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
let express = require('express')
let app = express()
let whitList = ['http://localhost:3000']
app.use(function(req, res, next) {
let origin = req.headers.origin
if (whitList.includes(origin)) {

res.setHeader('Access-Control-Allow-Origin', origin)

res.setHeader('Access-Control-Allow-Headers', 'name')

res.setHeader('Access-Control-Allow-Methods', 'PUT')

res.setHeader('Access-Control-Allow-Credentials', true)

res.setHeader('Access-Control-Max-Age', 6)

res.setHeader('Access-Control-Expose-Headers', 'name')
if (req.method === 'OPTIONS') {
res.end()
}
}
next()
})
app.put('/getData', function(req, res) {
console.log(req.headers)
res.setHeader('name', 'ming')
res.end('早上好')
})
app.get('/getData', function(req, res) {
console.log(req.headers)
res.end('早上好')
})
app.use(express.static(__dirname))
app.listen(4000)

Cors 与 Jsonp 比较

  1. cors 比 Jsonp 更强大
  2. Jsonp 只支持 Get 请求,cors 支持所有类型的 HTTP 请求
  3. 使用 cors,可以使用 XMLHttpRequest 发起请求和获取数据,比 Jsonp 有更好的错误处理
  4. Jsonp 的优势在于支持老式浏览器和可以向 cors 的网络请求数据
  5. cors 与 Jsonp 相比,更方便可靠

postMessage

什么是 postMessage

postMessage方法允许来自不同源的脚本采用异步方式进行有限的通信,可以实现跨文本档、多窗口、跨域消息传递。

postMessage 语法

1
otherWindow.postMessage(message, targetOrigin, [transfer])
  • otherWindow:其它窗口 (目标窗口) 的引用,比如 iframe 的 contentWindow 属性、执行 window.open 返回的窗口对象、或者是命名过或数值索引的 window.frames
  • message:将要发送到其他 window 的数据,这个数据会自动被序列化,数据格式也不受限制 (字符串,对象都可以)
  • targetOrigin:目标窗口的源,可以是字符串 * 表示无限制,或 URL, 需要协议端口号和主机都匹配才会发送
  • transfer(可选):是一串和 message 同时传递的 Tranferable 对象,这些对象的所有权将 被转移给消息接收方,而发送一方将不再保有所有权

兼容性

高级浏览器Internet Explorer 8+, chromeFirefox , OperaSafari 都将支持这个功能

流程实现

a.html 向 b.html 传递 “早上好”,然后 a.html 传回 “今天天气真好”

a.html

1
2
3
4
5
6
7
8
9
10
<iframe src="http://localhost:4000/b.html" frameborder="0" id="frame" onload="load()"></iframe>
<script>
function load(){
let frame = document.getElementById('frame');
frame.contentWindow.postMessage('早上好','http://localhost:4000');
window.onmessage=function(e){
console.log(e.data)
}
}
</script>

b.html

1
2
3
4
5
6
<script>
window.onmessage = function(e){
console.log(e.data);
e.source.postMessage('今天天气不错',e.origin)
}
</script>

a.js

1
2
3
4
let express = require('express')
let app = express();
app.use(express.static(__dirname));
app.listen(3000)

b.js

1
2
3
4
let express = require('express')
let app = express();
app.use(express.static(__dirname));
app.listen(4000)

Window.name

什么是 Window.name

window.name 是一个 window 对象的内置属性,name 属性可以设置或返回存放窗口的名称的一个字符串。

该属性的特征

在页面在浏览器端展示的时候,我们总能在控制台拿到一个全局变量 window,该变量有一个 name 属性,有以下的特征:

  1. 每个窗口都有独立的 window.name 与之对应
  2. 在一个窗口的生命周期中 (被关闭前),窗口载入的所有页面同时共享一个 window.name,每个页面 window.name 都有读写的权限
  3. window.name 一直存在与当前窗口,即使是新的页面载入也不会改变 window.name 的值
  4. window.name 可以存储不超过 2M 的数据,数据个数按需自定义

流程实现

  1. a.html 和 b.html 是同域 http://localhost:3000
  2. c.html 是独立的 http://localhost:4000
  3. a 获取 c 的数据
  4. a 先引用 c
  5. c 把值放到window.name, 把 a 引用的地址改为 b

a.html

1
2
3
4
5
6
7
8
9
10
11
12
13
<iframe src="http://localhost:4000/c.html" frameborder="0" onload="load()" id="iframe"></iframe>
<script>
let first = true
function load(){
if(first){
let iframe = document.getElementById('iframe');
iframe.src="http://localhost:3000/b.html";
first = false;
}else{
console.log(iframe.contentWindow.name)
}
}
<script>

b.html

1
2
3
<body>
早上好
</body>

c.html

1
2
3
<script>
window.name='今天天气不错'
</script>

a.js

1
2
3
4
let express = require('express')
let app = express();
app.use(express.static(__dirname));
app.listen(3000)

b.js

1
2
3
4
let express = require('express')
let app = express();
app.use(express.static(__dirname));
app.listen(4000)

location.hash

什么是 location.hash

locationjavascript里面的内置对象,如location.href就管理页面的 url,用loaction.href=url就可以直接将页面重定向 url, 而location.hash则可以用来获取或设置页面的标签值,hash 属性是一个可读可写的字符串,该字符串是 URL 的锚部分 (从 #号开始的部分)

location.hash 的简单应用

#的含义

#代表网页中的位置,其右边的字符,就是该位置的标识符,例如:

1
http:

就是代表 index.html 的 drafts 位置,浏览器读取这个 URL 后,会自动将 print 位置滚动至可视区域

HTTP 请求不包括#

#是用来指导浏览器的动作的,对服务器端完全无用,所以,HTTP 请求中不包括#
例如:

1
http:

浏览器实际发出的请求是这样的:

1
2
GET/index.html HTTP/1.1
Host:www.juejin.com

可以看到,只是请求的 index.html, 没有 #drafts 部分

#后的字符

在第一个 #出现的任何字符,都会被浏览器解读为位置标识符,这意味着,这些字符不会被发送到服务器端

改变 #不触发网页重构

单单改变 #后的部分,浏览器只会滚动到相应的位置,不会重新加载网页

改变 #会改变浏览器的访问历史

每一次改变 #后的部分,都会在浏览器的访问历史中增加一个记录,使用 “后退” 按钮,就可以回到上一个位置

读取 #值

window.location.hash 这个属性可读可写。读取时,可以用来判断网页状态是否改变;写入时,则会在不重载网页的前提下,创造一条访问历史记录

onhashchange 事件

当 #值发生变化时,就会触发这个事件。IE8+、Firefox 3.6+、Chrome 5+、Safari 4.0 + 支持该事件

Google 抓取 #的机制

默认情况下,Google 的网络忽视 URL 的 #部分

流程实现

路径后面的 hash 值可以用来通信。目的:a.html 想访问 c.html跨域相互通信。

  1. a.html 给 c.html 传一个 hash 值, 需要通过中间的 b.html 来实现
  2. c.html 收到 hash 值后 c.html 把 hash 值传递给 b.html
  3. b.html 将结果放到 a.html 的 hash 值中

a.html

1
2
3
4
5
6
<iframe src="http://localhost:4000/c.html#goodmorning"></iframe>
<script>
window.onhashchange = function () {
console.log(location.hash);
}
</script>

b.html

1
2
3
<script>
window.parent.parent.location.hash = location.hash
</script>

c.html

1
2
3
4
5
6
<script>
console.log(location.hash);
let iframe = document.createElement('iframe');
iframe.src = 'http://localhost:3000/b.html#goodevening';
document.body.appendChild(iframe);
</script>

a.js

1
2
3
4
let express = require('express')
let app = express();
app.use(express.static(__dirname));
app.listen(3000)

b.js

1
2
3
4
let express = require('express')
let app = express();
app.use(express.static(__dirname));
app.listen(4000)

domain

什么是 domain

主要用于主域相同的域之间的数据通信,注意 仅限主域相同,子域不同的跨域应用场景。

实现的原理:两个页面都通过 js 强制设置 document.domain 为基础主域,就实现了同域

说明

这个方法只能用于二级域名相同的情况下, 比如:

1
2
www.baidu.com
hhh.baidu.com

这就适用于 domain 方法

流程实现

a.html

1
2
3
4
5
6
7
<iframe src="http://b.ming.cn:3000/b.html" frameborder="0" onload="load()" id="frame"></iframe>
<script>
document.domain = 'ming.cn'
function load() {
console.log(frame.contentWindow.a);
}
</script>

b.html

1
2
3
4
5
6
7
<div>
早上好啊
</div>
<script type="text/javascript">
document.domain = 'ming.cn'
var a = 99999;
</script>

a.js

1
2
3
4
5
6
let express = require('express')
let app = express();
app.use(express.static(__dirname));
app.listen(3000, () => {
console.log('server run at 3000')
})

b.js

1
2
3
4
5
6
let express = require('express')
let app = express();
app.use(express.static(__dirname));
app.listen(4000, () => {
console.log('server run at 4000')
})


这个就可以通过 http://a.ming.cn:3000/a.html 获取到页面 http://a.ming.cn:3000/b.htm 中的 a 的值 99999

注意:这里我把我电脑上边的 hosts 修改了一下,不然不能出来效果

WebSocket

什么是 WebSocket

WebSocket是一种网络通信协议,它实现了浏览器与服务器全双工通信,同时允许跨域通讯。原生 WebSocket API 使用起来不太方便,我们使用Socket.io,它很好地封装了 webSocket 接口,提供了更简单、灵活的接口,也对不支持 webSocket 的浏览器提供了向下兼容。

WebSocket 如何工作

Web 浏览器和服务器都必须实现 WebSockets 协议来建立和维护连接。由于 WebSockets 连接长期存在,与典型的 HTTP 连接不同,对服务器有重要的影响。

注意:基于多线程或多进程的服务器无法适用于WebSockets,因为它旨在打开连接,尽可能快地处理请求,然后关闭连接。任何实际的WebSockets服务器端实现都需要一个异步服务器。

流程实现

a.html

1
2
3
4
5
6
7
8
9
10
<script>

let socket = new WebSocket('ws://localhost:3000');
socket.onopen=function(){
socket.send('早上好啊')
}
socket.onmessage = function(e){
console.log(e.data);
}
</script>

a.js

1
2
3
4
5
6
7
8
9
10
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('今天天气真好')
})
})


总结

以上就是整理的一些跨域的方法,我觉得一般用 cors,jsonp 等常见的方法就可以了,不过遇到了一些特殊情况,我们也要做到有很多方法是可以选择的,相信这篇文字会对大家有帮助!