什么是跨域?
浏览器同源策略: 即要求“协议、域名、端口”必须相同,同源策略是浏览器的一个安全功能,不同源的客户端脚本在没有明确授权的情况下,不能读写对方资源
只要通信中协议、域名、端口中有任意一个不同,就称之为跨域。同源限制是浏览器的行为,实际上双方通信是通的,但浏览器会拦截让客户端收不到服务器返回的信息。
一般跨域会在浏览器的console日志中会提示错误:
Access to XMLHttpRequest at 'http://localhost:4000/getData' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
还有可能在Network请求中提示错误:
Name | status |
---|---|
getData | CORS error |
如我们常见的ajax请求,就不支持跨域
请求跨域解决方案
jsonp
再说jsonp之前,我们先了解下不受跨域影响的标签,简单来说,就是带src的标签,如img, script等,如下例子:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- script引入不同源的vue文件 -->
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="http://localhost:4000/getData"></script>
<title>Document</title>
</head>
<body>
<!-- img标签 -->
<img src="https://ss0.bdstatic.com/70cFvHSh_Q1YnxGkpoWK1HF6hhy/it/u=2582512942,3155345292&fm=26&gp=0.jpg" alt="">
<script>
console.log(Vue) // 可以直接执行引入的script脚本
console.log(a) // 20
</script>
</body>
</html>
// 服务器:http://localhost:4000
router.get('/getData', ctx => {
ctx.body = "var a=20;"
})
而jsonp,就是利用了script标签引用js文件不受同源策略影响的原理,通过动态创建script标签来实现的。实现做法是:
- 客户端动态创建一个script标签,给其添加src属性,写上跨域url,并创建一个回调函数的querystring,比如定义为callback=myfunction, 并将该script标签添加到body上元素上
- 定义要执行的回调函数myfunction
- 服务器在接收到请求后,拿到对应资源后,通过键名callback拿到前端传递的方法名,并返回一个该回调函数执行的指令给客户端(对服务器来说,执行函数的指令是个字符串,所以不会执行)
- 客户端拿到服务器的应答时,就会执行这个回调函数,从而获取对应的资源
缺点:
- 只支持get请求,限制了参数大小和类型
- 请求过程无法终止,导致弱网络下处理超时请求比较麻烦
- 无法捕获服务端返回的异常信息
<!-- 客户端 -->
<body>
<button onclick="sendJsonp()">通过jsonp解决跨域</button>
<script>
// 1. JSONP
function sendJsonp() {
const script = document.createElement('script')
// 通过querystring的方式,传递一个回调函数的参数,这个回调参数的键值是前后端一起定义的并保持一致的
script.src = "http://localhost:4000/jsonpData?callback=customFunc"
document.body.appendChild(script)
}
// 自定义的函数名,即使用jsonp传递给后台的回调函数名
function customFunc(res) {
console.log(res) // 1. 200; 2. {name: "hahha", age: 3}
}
</script>
</body>
// 服务器端
const Koa = require("koa")
const Router = require("koa-router")
const app = new Koa()
const router = new Router()
router.get('/jsonpData', ctx => {
const callback = ctx.query.callback
// ctx.body = `${callback}(200)` // 后端拿到前端传递过来的函数名后,返回一个函数执行的指令给前端,前端拿到后会立即执行该函数
// 如果参数是一个对象,那要将其转换成字符串
let obj = {
name: 'hahha',
age: 3
}
let objStr = JSON.stringify(obj)
ctx.body = `${callback}(${objStr})` // 注意,如果参数直接传objStr,客户端会认为这是一个变量,会报未找到异常
})
app.use(router.routes())
app.listen(4000)
CORS解决跨域
CORS(cross-origin resource sharing),跨域资料共享,是浏览器为AJAX请求设置的一种跨域机制,让其可以在服务端允许的情况下进行跨域访问。它比jsonp更加优雅。
它主要是通过设置http响应头来告诉浏览器,服务端是否允许当前域的脚本进行跨域访问。
跨域资源共享将AJAX请求分为了两类:简单请求和复杂请求。
简单请求
符合以下两个特征:
- 请求方法为head、get、post
- 请求头只接受以下字段:
- Accept:浏览器能够接受的响应内容类型
- Accept-Language: 浏览器能接受的自然语言列表
- Content-Type: 请求对应的类型,只能为以下三种:
1)text/plain- multipart/form-data
- application/x-www-form-urlencoded
- Content-Language:浏览器希望采用的自然语言
- Save-Data:浏览器是否希望减少数据传输量
对于简单请求:
- 浏览器发出简单请求时,会在请求头增加一个origin字段,值为请求源的信息;
- 服务器收到请求后,根据请求头origin判断,返回相应的内容
- 浏览器收到响应后,根据响应头Access-Control-Allow-Origin进行判断,这个字段是服务端允许跨域请求的源,如果响应头没有包含这个字段或者这个响应头中的值没有包含当前源,则会抛出错误;如果有,则是允许当前源进行跨域请求。
复杂请求
只要不满足简单请求特征中的任意一条,就属于复杂请求
对于复杂请求:
- 会预先发个
options
预检请求,浏览器会在请求头添加Access-control-Request-Method
字段,值为跨域请求的请求方法,用于探查目标接口,允许那些请求方式; - 如果添加了不属性于简单请求的头部字段,浏览器还会添加一个
Access-Control-Request-Headers
字段,值为跨域请求添加的请求头部字段 - 服务器接收到请求后,除了会返回
Access-Control-Allow-Origin
的字段外,还会根据请求头,返回对应的响应头Access-control-Request-Methods
和Access-Control-Allow-Headers
,告诉浏览器服务端允许的源、方法和请求头字段,并返回 204 状态码。 - 浏览器得到预检请求的响应后,会判断当前请求是否在服务端的许可范围内,如果在,则继续发送跨域请求;否则,则直接报错
Websocket
Websocket 是 HTML5 规范提出的一个应用层的全双工协议,适用于浏览器与服务器进行实时通信场景。
什么叫全双工呢?
这是通信传输的一个术语,这里的“工”指的是通信方向,“双工”是指从客户端到服务端,以及从服务端到客户端两个方向都可以通信,“全”指的是通信双方可以同时向对方发送数据。与之相对应的还有半双工和单工,半双工指的是双方可以互相向对方发送数据,但双方不能同时发送,单工则指的是数据只能从一方发送到另一方。
WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
Websocket的实现:
一个网页创建一个WebSocket连接,连接到另一个网页(或服务器),然后调用send()方法向另一个网页发送消息,通过监听onmessage事件得到另一个网页发送的消息。
if ("WebSocket" in window) {
// 创建一个连接另一个网页的ws实例
var ws = new WebSocket("ws://b.com");
// 连接建立时触发的事件
ws.onopen = function(){
// 发送消息
ws.send(...);
}
ws.onmessage = function(e){
// 接收消息
console.log(e.data);
}
// 关闭连接
ws.close()
}else {
alert("您的浏览器不支持 WebSocket!");
}
代理转发
既然同源策略是浏览器设置的安全策略,那么,我们只要不通过浏览器直接发送请求,而是通过服务器来发送请求,那么就不存在同源限制了。
所以我们可以把这个模式转换下:
浏览器 -> 不同源服务器 发送请求
改为:
浏览器 -> 同源服务器 -> 不同源服务器 发请求
这就是我们说的代理转发的原理。
在客户端使用的代理称为“正向代理”,在服务端设置的代理叫做“反向代理”。代理转发实现起来非常简单,在当前被访问的服务器配置一个请求转发规则就行了。
// 正向代理
// webpack.config.js
module.exports = {
//...
devServer: {
proxy: {
'/api': 'http://localhost:3000'
}
}
};
在 Nginx 服务器上配置同样的转发规则也非常简单,下面是示例配置(反向代理)。
通过 location 指令匹配路径,然后通过 proxy_pass 指令指向代理地址即可。
location /api {
proxy_pass http://localhost:3000;
}
页面跨域解决方案
除了浏览器请求跨域之外,页面之间也会有跨域需求,例如使用 iframe 时父子页面之间进行通信。
postMessage
HTML5 推出了一个新的函数 postMessage() 用来实现父子页面之间通信,而且不论这两个页面是否同源。
实现,父页面向子页面发消息
// http://www.fahter.com
// 父页面打开子页面
let son = window.open('http://www.son.com')
// 父页面向子页面发消息
son.postMessage('I am your father', 'http://www.son.com');
// http://www.son.com
// 子页面通过监听message获取父页面的消息
window.addEventListener('message', function(e) {
console.log(e.data);
},false);
// 子页面通过window.opener.postMessage给父页面发消息
window.opener.postMessage('I am your son', 'http://www.fahter.com');
修改域名document.domain
由于JavaScript同源策略的限制,脚本只能读取和所属文档来源相同的窗口和文档的属性。
对于已经有成熟产品体系的公司来说,不同的页面可能放在不同的服务器上,这些服务器域名不同,但是拥有相同的上级域名,比如id.qq.com、www.qq.com、user.qzone.qq.com,它们都有公共的上级域名qq.com。这些服务器上的页面之间的跨域访问可以通过document.domain来进行。
默认情况下,document.domain存放的是载入文档的服务器的主机名,可以手动设置这个属性,不过是有限制的,只能设置成当前域名或者上级的域名,并且必须要包含一个.号,也就是说不能直接设置成顶级域名。例如:id.qq.com,可以设置成qq.com,但是不能设置成com。
具有相同document.domain的页面,就相当于是处在同域名的服务器上,如果协议和端口号也是一致,那它们之间就可以跨域访问数据。