「深入浅出」前端开发中常用的几种跨域解决方案

开发
本文将为大家介绍,前端开发中,最常用的几种跨域解决方案;

看完本文可以系统地掌握,不同种跨域解决方案间的巧妙,以及它们的用法、原理、局限性和适用的场景

[[342546]]

包括以下几个方面:

跨域的现象,和几种常见的跨域表现
跨域的解决方案(原理分析)
修改本地HOST
JSONP
CORS
Proxy
Nginx反向代理
Post Message(利用iframe标签,实现不同域的关联)
同源是什么?
如果两个URL的协议protocol、主机名host和端口号port都相同的话,则这两个URL是同源。

同源策略
同源策略是一个重要的安全策略。它能够阻断恶意文档,减少被攻击的媒介。

真实项目中,很少有同源策略,大部分都是非同源策略

跨域是什么?
当协议、域名与端口号中任意一个不相同时,都算作不同域,不同域之间相互请求资源的表现(非同源策略请求),称作”跨域“。

跨域现象
那么我们就下面的网址分析一下,哪一块是协议,哪一块是域名及端口号 

  1. http://kbs.sports.qq.com/index.html 
  2.  
  3. 协议:http(还有以一种https协议) 
  4. 域名:kbs.sports.qq.com 
  5. 端口号:80 
  6.  
  7. https://127.0.0.1:3000 
  8.  
  9. 协议:https 
  10. 域名:127.0.0.1 
  11. 端口号:3000 

假如我们的真实项目开发中的Web服务器地址为 ”http://kbs.sports.qq.com/index.html“,而需要请求的数据接口地址为 "http://api.sports.qq.com/list"。

当Web服务器的地址向数据接口的地址发送请求时,便会造成了跨域现象

造成跨域的几种常见表现
服务器分开部署(Web服务器 + 数据请求服务器)
本地开发(本地预览项目 调取 测试服务器的数据)
调取第三方平台的接口
Web服务器:主要用来静态资源文件的处理

解决方案

  1. 修改本地HOST(不作介绍) 
  2. JSONP 
  3. CORS 
  4. Proxy 
  5. Nginx反向代理 
  6. Post Message(利用iframe标签,实现不同域的关联) 

在后面会详细分析这四种解决方案的原理和用法配置,以及它们的优点和局限性

  1. 注意: 基于ajax或fetch发送请求时,如果是跨域的,则浏览器默认的安全策略会禁止该跨域请求 

  1. 补充说明:以下所有的测试用例,均由Web:http://127.0.0.1:5500/index.html向API:http://127.0.0.1:1001/list发起请求 

API接口的服务器端是自己通过express建立的,下文在服务器端以app.use中间件的形式接受来自客户端的请求并做处理。

  1. 即 在“http://127.0.0.1:1001/list”from origin“http://127.0.0.1:55”上对XMLHttpRequest的访问已被CORS策略阻止:被请求的资源上没有“Access- control - allow-origin”头 

在后端开启了一个端口号为1001的服务器之后,我们来实践一下

  1. let xhr = new XMLHttpRequest; 
  2. xhr.open('get''http://127.0.0.1:1001/list'); 
  3. xhr.onreadystatechange = () => { 
  4.   if (xhr.status === 200 && xhr.readyState === 4) { 
  5.     console.log(xhr.responseText); 
  6.   } 
  7. }; 
  8. xhr.send();  

跨域的常见报错提示
这就是由于浏览器默认的安全策略禁止导致的。

下面介绍一下几种常见的解决方案。

JSONP
原理:JSONP利用script标签不存在域的限制,且定义一个全局执行上下文中的函数func

(用来接收服务器端返回的数据信息)来接收数据,从而实现跨域请求。

  1. 弊端: 
  2.  
  3. 只允许GET请求 
  4. 不安全:只要浏览器支持,且存在浏览器的全局变量里,则谁都可以调用 

图解JSONP的原理

手动封装JSONP
callback必须是一个全局上下文中的函数

(防止不是全局的函数,我们需要把这个函数放在全局上,并且从服务器端接收回信息时,要浏览器执行该函数)

注意:

uniqueName变量存储全局的回调函数(确保每次的callback都具有唯一性)
检验url中是否含有"?",有的话直接拼接callback,没有的话补”?“

  1. // 客户端 
  2. function jsonp(url, callback) { 
  3.   // 把传递的回调函数挂载到全局上 
  4.  let uniqueName = `jsonp${new Date().getTime()}`; 
  5.   // 套了一层 anonymous function 
  6.   // 目的让 返回的callback执行且删除创建的标签 
  7.   window[uniqueName] = data => { 
  8.   // 从服务器获取结果并让浏览器执行callback 
  9.     document.body.removeChild(script); 
  10.     delete window[uniqueName]; 
  11.     callback && callback(data); 
  12.   } 
  13.    
  14.   // 处理URL 
  15.   url += `${url.includes('?')} ? '&' : '?}callback=${uniqueName}'`; 
  16.    
  17.   // 发送请求 
  18.   let script = document.createElement('script'); 
  19.   script.src = url; 
  20.   document.body.appendChild(script); 
  21.  
  22. // 执行第二个参数 callback,获取数据 
  23. jsonp('http://127.0.0.1:1001/list?userName="lsh"', (result) => { 
  24.  console.log(result); 
  25. }) 
  1. // 服务器端 
  2. // Api请求数据 
  3. app.get('/list', (req, res) => { 
  4.    
  5.   // req.query 问号后面传递的参数信息 
  6.   // 此时的callback 为传递过来的函数名字 (uniqueName) 
  7.  let { callback } = req.query; 
  8.    
  9.   // 准备返回的数据(字符串) 
  10.   let res = { code: 0, data: [10,20] }; 
  11.   let str = `${callback}($(JSON.stringify(res)))`; 
  12.    
  13.   // 返回给客户端数据 
  14.   res.send(str); 
  15. }) 

测试用例展示:

客户端请求的url
服务端返回的数据
返回的callback
返回的数据信息 result

  1. // 服务器请求的 url 
  2. Request URL: 
  3.  http://127.0.0.1:1001/list?userName="lsh"&callback=jsonp159876002 
  4.  
  5. // 服务器返回的函数callback 
  6.  jsonp159876002({"code":0, "data": [10,20]}); 
  7.  
  8. // 客户端接收的数据信息 
  9. { code: 0, data: Array(2) } 

当浏览器发现返回的是jsonp159876002({"code":0, "data": [10,20]});这个函数,会自动帮我们执行的。

JSONP弊端
在上文中说到只要服务器端那里设置了允许通过jsonp的形式跨域请求,我们就可以取回数据。

下面是在我们封装完jsonp方法之后,向一个允许任何源向该服务器发送请求的网址xxx

  1. jsonp('https://matchweb.sports.qq.com/matchUnion/cateColumns?from=pc', result => { 
  2.   console.log(result); 
  3. }); 

CORS
上文提到,不允许跨域的根本原因是因为Access-Control-Allow-Origin已被禁止

那么只要让服务器端设置允许源就可以了

原理:解决掉浏览器的默认安全策略,在服务器端设置允许哪些源请求就可以了

先来看一下下面的设置有哪些问题

  1. // 服务器端 
  2. app.use((req, res, next) => { 
  3.  // * 允许所有源(不安全/不能携带资源凭证) 
  4.  res.header("Access-Control-Allow-Origin""*"); 
  5.  res.header("Access-Control-Allow-Credentials"true); 
  6.  
  7.  /* res.header("Access-Control-Allow-Headers""Content-Type,...."); 
  8.  res.header("Access-Control-Allow-Methods""GET,..."); */ 
  9.  
  10.  // 试探请求:在CORS跨域请求中,首先浏览器会自己发送一个试探请求,验证是否可以和服务器跨域通信,服务器返回200,则浏览器继续发送真实的请求 
  11.  req.method === 'OPTIONS' ? res.send('CURRENT SERVICES SUPPORT CROSS DOMAIN REQUESTS!') : next(); 
  12. }); 
  13.  
  14. // 客户端 
  15. let xhr = new XMLHttpRequest; 
  16. xhr.open('get''http://127.0.0.1:1001/list'); 
  17. xhr.setRequestHeader('Cookie''name=jason'); 
  18. xhr.withCredentials = true
  19. xhr.onreadystatechange = () => { 
  20.   if (xhr.status === 200 && xhr.readyState === 4) { 
  21.     console.log(xhr.responseText); 
  22.   } 
  23. }; 
  24. xhr.send(); 

当我们一旦在服务器端设置了允许任何源可以请求之后,其实请求是不安全的,并且要求客户端不能携带资源凭证(比如上文中的Cookie字段),浏览器端会报错。

告诉我们Cookie字段是不安全的也不能被设置的,如果允许源为'*'的话也是不允许的。

假如在我们的真实项目开发中

正确写法✅

设置单一源(安全/也可以携带资源凭证/只能是单一一个源)
也可以动态设置多个源:每一次请求都会走这个中间件,我们首先设置一个白名单,如果当前客户端请求的源在白名单中,我们把Allow-Origin动态设置为当前这个源

  1. app.use((req, res, next) => { 
  2.    
  3.   // 也可自定义白名单,检验请求的源是否在白名单里,动态设置 
  4.   /* let safeList = [ 
  5.   "http://127.0.0.1:5500"
  6.   xxx, 
  7.   xxxxx, 
  8.  ]; */ 
  9.  res.header("Access-Control-Allow-Origin""http://127.0.0.1:5500"); 
  10.  res.header("Access-Control-Allow-Credentials"true); // 设置是否可携带资源凭证 
  11.  
  12.  /* res.header("Access-Control-Allow-Headers""Content-Type,...."); 
  13.  res.header("Access-Control-Allow-Methods""GET,..."); */ 
  14.  
  15.  // 试探请求:在CORS跨域请求中,首先浏览器会自己发送一个试探请求,验证是否可以和服务器跨域通信,服务器返回200,则浏览器继续发送真实的请求 
  16.  req.method === 'OPTIONS' ? res.send('CURRENT SERVICES SUPPORT CROSS DOMAIN REQUESTS!') : next(); 
  17. }); 

CORS的好处
原理简单,容易配置,允许携带资源凭证
仍可以用 ajax作为资源请求的方式
可以动态设置多个源,通过判断,将Allow-Origin设置为当前源
CORS的局限性
只允许某一个源发起请求
如多个源,还需要动态判断
Proxy
Proxy翻译为“代理”,是由webpack配置的一个插件,叫"webpack-dev-server"(只能在开发环境中使用)

Proxy在webpack中的配置

  1. const path = require('path'); 
  2. const HtmlWebpackPlugin = require('html-webpack-plugin'); 
  3.  
  4. module.exports = { 
  5.   mode: 'production'
  6.   entry: './src/main.js'
  7.   output: {...}, 
  8.   devServer: { 
  9.     port: '3000'
  10.     compress: true
  11.     opentrue
  12.     hot: true
  13.     proxy: { 
  14.       '/': { 
  15.         target: 'http://127.0.0.1:3001'
  16.         changeOrigin: true 
  17.       } 
  18.     } 
  19.   }, 
  20.   // 配置WEBPACK的插件 
  21.   plugins: [ 
  22.     new HtmlWebpackPlugin({ 
  23.       template: `./public/index.html`, 
  24.       filename: `index.html` 
  25.     }) 
  26.   ] 
  27. }; 

图解Proxy的原理

Proxy代理其实相当于由webpack-dev-server配置在本地创建了一个port=3000的服务,利用node的中间层代理(分发)解决了浏览器的同源策略限制。

但是它只能在开发环境下使用,因为dev-server只是一个webpack的一个插件;

如果需要在生产环境下使用,需要我们配置Nginx反向代理服务器;

另外如果是自己实现node服务层代理:无论是开发环境还是生产环境都可以处理(node中间层和客户端是同源,中间层帮助我们向服务器请求数据,再把数据返回给客户端)

Proxy的局限性
只能在本地开发阶段使用

配置Nginx反向代理
主要作为生产环境下跨域的解决方案。

原理:利用Node中间层的分发机制,将请求的URL转向服务器端的地址

配置反向代理

  1. server { 
  2.  listen: 80; 
  3.   server_name: 192.168.161.189; 
  4.   loaction: { 
  5.   proxy_pass_http://127.0.0.1:1001; // 请求转向这个URL地址,服务器地址 
  6.     root html; 
  7.     index index.html; 
  8.   } 

简单写了一下伪代码,实际开发中根据需求来配。

POST MESSAGE
假设现在有两个页面,分别为A页面port=1001、B页面port=1002,实现页面A与页面B的页面通信(跨域)

原理:

把 B页面当做A的子页面嵌入到A页面里,通过iframe.contentWindow.postMessage向B页面传递某些信息
在A页面中通过window.onmessage获取A页面传递过来的信息ev.data(见下代码)
同理在B页面中通过ev.source.postMessage向A页面传递信息
在A页面中通过window.onmessage获取B页面传递的信息
主要利用内置的postMessage和onmessage传递信息和接收信息。

A.html

  1. // 把 B页面当做A的子页面嵌入到A页面里 
  2. <iframe id="iframe" src="http://127.0.0.1:1002/B.html" frameborder="0" style="display: none;"></iframe> 
  3.  
  4. <script> 
  5.   iframe.onload = function () { 
  6.     iframe.contentWindow.postMessage('珠峰培训''http://127.0.0.1:1002/'); 
  7.   } 
  8.  
  9.   //=>监听B传递的信息 
  10.   window.onmessage = function (ev) { 
  11.     console.log(ev.data); 
  12.   } 
  13. </script> 

B.html

  1. <script> 
  2.   //=>监听A发送过来的信息 
  3.   window.onmessage = function (ev) { 
  4.     // console.log(ev.data); 
  5.  
  6.     //=>ev.source:A 
  7.     ev.source.postMessage(ev.data + '@@@''*'); 
  8.   } 
  9. </script> 

几种方案的比较
1. JSONP方案需要前后端共同配置完成(利用script标签不存在域的限制)【麻烦,老项目使用】

2. CORS原理简单,但只能配置单一源,如果需要配置多个源,也只能从白名单中筛选出某一个符合表求的origin【偶尔使用】

服务器端需要单独做处理,客户端较为简单

3. Proxy客户端通过dev-server,生产环境需要配置Nginx反向代理(利用Node中间层分发机制)【常用】

 

 

责任编辑:姜华 来源: 前端时光屋
相关推荐

2012-03-27 15:23:15

JSONPAJAX

2023-05-06 15:32:04

2010-07-26 12:57:12

OPhone游戏开发

2021-03-16 08:54:35

AQSAbstractQueJava

2018-12-12 15:50:13

2011-07-04 10:39:57

Web

2010-07-26 13:55:10

OPhone游戏开发

2018-03-15 09:13:43

MySQL存储引擎

2019-11-11 14:51:19

Java数据结构Properties

2009-11-30 16:46:29

学习Linux

2022-12-02 09:13:28

SeataAT模式

2019-01-07 15:29:07

HadoopYarn架构调度器

2017-07-02 18:04:53

块加密算法AES算法

2012-05-21 10:06:26

FrameworkCocoa

2021-07-20 15:20:02

FlatBuffers阿里云Java

2022-09-26 09:01:15

语言数据JavaScript

2023-03-20 09:48:23

ReactJSX

2012-02-21 13:55:45

JavaScript

2009-11-18 13:30:37

Oracle Sequ

2018-11-09 16:24:25

物联网云计算云系统
点赞
收藏

51CTO技术栈公众号