浏览器跨域及跨域解决方案

什么是跨域?

浏览器同源策略: 即要求“协议、域名、端口”必须相同,同源策略是浏览器的一个安全功能,不同源的客户端脚本在没有明确授权的情况下,不能读写对方资源

只要通信中协议、域名、端口中有任意一个不同,就称之为跨域。同源限制是浏览器的行为,实际上双方通信是通的,但浏览器会拦截让客户端收不到服务器返回的信息。

一般跨域会在浏览器的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标签来实现的。实现做法是:

  1. 客户端动态创建一个script标签,给其添加src属性,写上跨域url,并创建一个回调函数的querystring,比如定义为callback=myfunction, 并将该script标签添加到body上元素上
  2. 定义要执行的回调函数myfunction
  3. 服务器在接收到请求后,拿到对应资源后,通过键名callback拿到前端传递的方法名,并返回一个该回调函数执行的指令给客户端(对服务器来说,执行函数的指令是个字符串,所以不会执行)
  4. 客户端拿到服务器的应答时,就会执行这个回调函数,从而获取对应的资源

缺点:

  1. 只支持get请求,限制了参数大小和类型
  2. 请求过程无法终止,导致弱网络下处理超时请求比较麻烦
  3. 无法捕获服务端返回的异常信息
<!-- 客户端 -->
<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请求分为了两类:简单请求复杂请求

简单请求

符合以下两个特征:

  1. 请求方法为head、get、post
  2. 请求头只接受以下字段:
    • Accept:浏览器能够接受的响应内容类型
    • Accept-Language: 浏览器能接受的自然语言列表
    • Content-Type: 请求对应的类型,只能为以下三种:
      1)text/plain
      1. multipart/form-data
      2. application/x-www-form-urlencoded
    • Content-Language:浏览器希望采用的自然语言
    • Save-Data:浏览器是否希望减少数据传输量

对于简单请求:

  1. 浏览器发出简单请求时,会在请求头增加一个origin字段,值为请求源的信息;
  2. 服务器收到请求后,根据请求头origin判断,返回相应的内容
  3. 浏览器收到响应后,根据响应头Access-Control-Allow-Origin进行判断,这个字段是服务端允许跨域请求的源,如果响应头没有包含这个字段或者这个响应头中的值没有包含当前源,则会抛出错误;如果有,则是允许当前源进行跨域请求。

复杂请求

只要不满足简单请求特征中的任意一条,就属于复杂请求

对于复杂请求:

  1. 会预先发个options预检请求,浏览器会在请求头添加Access-control-Request-Method字段,值为跨域请求的请求方法,用于探查目标接口,允许那些请求方式;
  2. 如果添加了不属性于简单请求的头部字段,浏览器还会添加一个Access-Control-Request-Headers字段,值为跨域请求添加的请求头部字段
  3. 服务器接收到请求后,除了会返回Access-Control-Allow-Origin的字段外,还会根据请求头,返回对应的响应头Access-control-Request-MethodsAccess-Control-Allow-Headers,告诉浏览器服务端允许的源、方法和请求头字段,并返回 204 状态码。
  4. 浏览器得到预检请求的响应后,会判断当前请求是否在服务端的许可范围内,如果在,则继续发送跨域请求;否则,则直接报错

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.comwww.qq.comuser.qzone.qq.com,它们都有公共的上级域名qq.com。这些服务器上的页面之间的跨域访问可以通过document.domain来进行。

默认情况下,document.domain存放的是载入文档的服务器的主机名,可以手动设置这个属性,不过是有限制的,只能设置成当前域名或者上级的域名,并且必须要包含一个.号,也就是说不能直接设置成顶级域名。例如:id.qq.com,可以设置成qq.com,但是不能设置成com。

具有相同document.domain的页面,就相当于是处在同域名的服务器上,如果协议和端口号也是一致,那它们之间就可以跨域访问数据。

参考:https://blog.csdn.net/nlznlz/article/details/79506655

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容