深入解析跨域、带你实践跨域的解决方案

什么是跨域?

跨域报错

我在本地的8124端口起了一个node服务

js 复制代码
var http = require('http');
​
var server = http.createServer(function (req, res) {
    res.writeHead(200, {'Content-Type': 'text/plain'});
    res.end('Hello World\n');
});
server.listen(8124);

现在我期望我的cors.html中的js能够请求到这个接口

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>cors</title>
</head>
<body>
    <script>
        try{
            fetch("http://localhost:8124")
            .then(res => console.log(res));
        }catch(err){
            console.log('err', err);
        }
    </script>
</body>
</html>

预想中,应该会收到Hellow World,但是意外发生了,浏览器给我报了一个错。

用Postman测一下,是不是接口出问题了

一切正常。。。。那么到底出了什么问题呢------违反了浏览器的同源策略,也就是产生了跨域

同源策略

简单来说,同源策略,是浏览器的一种安全机制。浏览器会限制js中发起的跨源http请求。

url的组成

在说跨域之前,我们先来了解一下url(Uniform Resource Locator,统一资源定位器)的组成。

url一般包括以下几个部分

部分 作用
协议 指定了客户端和服务器之间通信的协议类型。常见的协议有HTTP、HTTPS、FTP等
域名 表示服务器的地址,可以是顶级域名(如 .com.org.net 等)或子域名(如 www.example.com 中的 www)。
端口号(可选) 用于指定服务器上特定服务的访问端口。如果省略,则使用默认端口 (例如,HTTP服务的默认端口是80,HTTPS服务的默认端口是443)
路径(可选) 指定了服务器上的特定资源。它可以是目录路径,也可以是页面路径。如果省略,通常表示访问服务器的默认页面。
查询字符串(可选) 用于向服务器发送一些额外的信息,通常用于动态网页。查询字符串以"?"开始,后面跟着参数名和参数值对。 例如:?name=value&anotherName=anotherValue
片段标识符(可选) 也被称为锚点,用于指定网页中的一个具体位置。

我们通过协议+域名+端口号就可以找到一个服务。

什么是同源?

现在有两个地址,如果他们的协议、域名、端口都一样,则称他们为同源。反之,有一个不一样则非同源。下面是几个例子:

源1 源2 是否同源 原因
http://a.com:81/a https://a.com:81/a 协议不同
http://`a.com`:81/a http://`www.a.com`:81/a 域名不同
a.com:81/a a.com:82/a 端口不同
a.com:81/a/b a.com:81/a/c

什么情况下会触发跨域?

以ajax请求为例子,当我们发出的请求收到服务器响应时,会先经过浏览器校验。如果是同源,则接收响应,反之则跨域报错。所以要注意两点:

1.跨域产生的原因在于浏览器校验,如果不是浏览器发出请求(比如postman),则不会有跨域问题。

2.产生跨域不代表浏览器没有得到请求响应,有可能只是被浏览器拦截了。

其实被限制的是非同源,不仅仅是非同域。所以与其称为跨域,称为跨源(orign)其实更加妥当。

方式 例子
网络通信 例如,在一个页面中通过AJAX请求另一个域名的数据接口,或者通过<img><script><link>等标签加载其他域的资源。
JS API window.openwindow.parentiframe.contentWindow等,当这些API涉及到的窗口或框架属于不同的源时,也会产生跨域问题。
存储 例如,当尝试从一个源访问另一个源的WebStorage(如localStorage或sessionStorage)或IndexedDB时,也可能遇到跨域问题。

关于标签:

等标签在加载资源时,并不总是会引起跨域问题。与严格限制ajax请求不同,浏览器对标签的跨域有轻微的限制。

例如:

  1. <img>标签:通常用于加载图片资源。在大多数情况下,使用<img>标签加载图片不会引发跨域问题,因为浏览器会默认允许图片的跨域加载。然而,如果需要对加载的图片进行更复杂的操作,如使用 Canvas API 进行像素级操作,那么就可能遇到跨域问题。这是因为出于安全考虑,浏览器限制了从不同源加载的图片在 Canvas 中的使用。
  2. <script>标签:当用于引入 JavaScript 文件时,通常不会引起跨域问题,因为浏览器允许从不同的源加载脚本。然而,有一种特殊情况是当<script>标签的类型被设置为module(即<script type="module">)时,它可能会受到浏览器的同源策略限制,从而引发跨域问题。此外,如果使用 JSONP(JSON with Padding)技术通过<script>标签跨域获取数据,则不会受到同源策略的限制。
  3. <link>标签:通常用于加载 CSS 样式表或其他关联资源。在大多数情况下,使用<link>加载 CSS 不会引发跨域问题。然而,如果 CSS 文件中包含了字体文件、背景图片等跨域资源,且这些资源的服务器没有正确配置 CORS(跨域资源共享)策略,那么可能会引发跨域问题。

关于JS API:

window.open 方法在打开新窗口或标签页时,通常不会直接受到同源策略的限制,因为它仅仅是创建了一个新的浏览器上下文来加载指定的URL。然而,在使用 window.open 打开的新页面中,如果尝试通过脚本与原页面进行交互,如访问或修改原页面的DOM、读取或修改原页面的cookies等,这时就会受到同源策略的限制。

为什么有同源策略

其实从触发同源策略的场景,我们就可以大概推测出来,浏览器为什么有同源机制------为了安全。

1.防止恶意网站通过跨域请求获取用户的敏感信息

此时,有一个恶意网站,其中的脚本企图获取浏览器打开的其它页面的cookie、session。其中有页面在cookie中存放了个人信息甚至是账号密码。

如果没有同源策略,那么这个恶意网站就可以轻松的获取到这些数据。

2.防止恶意网页读取或者修改另一个网页的内容

如果恶意网站的js脚本不受同源策略的限制,他就可以读取或者修改另一个网页的内容,比如修改另一个页面的dom结构,甚至是造一个假的登陆页面以骗取账号密码。

跨域常见解决方案

JSONP

JSONP (JSON with Padding) 是一种解决跨域数据交换的技术。它利用了<script>标签没有跨域限制的"漏洞"(因为浏览器允许从不同的源加载脚本)来实现跨域数据请求。

JSONP 的实现原理大致如下:

  1. 客户端创建<script>标签 :在客户端(通常是Web页面)上,开发者动态地创建一个<script>标签,并将其src属性设置为跨域的API端点。这个端点通常接受一个回调函数名作为参数。
  2. 服务器端响应 :服务器接收到请求后,生成一个包含数据的JavaScript函数调用,并将这个函数调用作为响应返回。这个函数调用的参数就是客户端请求的数据,以JSON格式提供。
  3. 客户端执行回调函数 :当服务器响应的<script>标签被浏览器加载并执行时,它会调用在全局作用域中定义的回调函数,并将JSON数据作为参数传递给这个函数。这样,客户端就可以处理这些数据了。
  4. 处理数据:在回调函数内部,客户端可以处理从服务器接收到的数据。

过程大致如下:

下面是一个简单的JSONP实现示例:

客户端:

js 复制代码
<script>
    // 建立一个全局函数,这个函数靠script请求返回的脚本进行调用 
    function jsonpCallback(data){
        console.log(data);
    }
</script>
// 这个脚步将请求的参数以及要调用的函数名交给后端
<script src="http://localhost:8124/getData?id=1&callback=jsonpCallback"></script>

服务端:

这里简单写了一个服务端,mock了一些数据,部署在8124端口。

js 复制代码
var http = require('http');
// 用于解析参数
function getParams(url) {
    const queryString = url.split('?')[1] ?? ''; // 获取查询字符串部分  
    const pairs = queryString.split('&'); // 将字符串按&分隔成键值对数组  
    const params = {};  
    for (let i = 0; i < pairs.length; i++) {  
    const pair = pairs[i].split('='); // 将键值对按=分隔  
    const key = decodeURIComponent(pair[0]); // 解码键  
    const value = decodeURIComponent(pair[1]); // 解码值  
    params[key] = value; // 存储键值对  
    }  
    return params;
}
// mock数据
const MockData = [
    {
        id: 1,
        name: 'ljm',
        age: 18,
    },
    {
        id: 2,
        name: 'hjl',
        age: 19,
    }
]
​
var server = http.createServer(function (req, res) {
    if(req.method == 'GET'){
        params = getParams(req.url);
        res.writeHead(200, {'Content-Type': 'text/plain'});
        const data = MockData.filter(item => item.id == params.id);
        if(data.length > 0){
            // 返回一个包含数据的函数
            res.end(`${params?.callback ?? ''}(${JSON.stringify(data)})`);
        }else{
            res.end('Not Found\n');
        }
    }else{
        res.writeHead(404);
        res.end();
    }
});
server.listen(8124);

效果:

jsonp实现起来相对复杂,需要和服务端做好约定才能实现。

并且,由于使用的是<script>标签实现的,所以只支持get请求。

而且这样实现出来的后端服务就只支持js实现,postman等测试工具只能拿到脚本数据,没法运行脚本。

所以并不是很鼓励使用这种方式。

代理服务器

什么是代理服务器?

代理服务器(Proxy Server)简单来说,就是用于中转用户的请求,充当一个中间件。

通常客户端与目标服务器之间进行沟通是直接沟通的

代理服务器就处于他们二者之间

代理服务器的主要目的是提供某种形式的网络服务,如缓存、负载均衡、安全性或访问控制等。

代理服务器与跨域

既然代理服务器可以转发请求,并且跨域限制只在浏览器上,所以代理服务器不受限制,那么我们就可以通过代理服务器绕开跨域限制。

只要我们起一个和浏览器同源的代理服务器,那么向它发送的请求就不会产生跨域问题,再经由它转发,就可以顺利解决跨域问题。这也是我们本地开发常使用的方式。

这里我们简单起一个react应用,安装axios,向服务端发起请求

客户端:

部署在3000端口

js 复制代码
import logo from './logo.svg';
import './App.css';
import axios from 'axios';
​
function App() {
  try{
    axios.get('http://localhost:8124/getData?id=1');
  } catch(err){
    console.log(err);
  }
  return (
    <div className="App">
      test
    </div>
  );
}
​
export default App;

服务端:

这个服务端同样部署在8124端口

js 复制代码
var http = require('http');
function getParams(url) {
    const queryString = url.split('?')[1] ?? []; // 获取查询字符串部分  
    const pairs = queryString.split('&'); // 将字符串按&分隔成键值对数组  
    const params = {};  
    for (let i = 0; i < pairs.length; i++) {  
    const pair = pairs[i].split('='); // 将键值对按=分隔  
    const key = decodeURIComponent(pair[0]); // 解码键  
    const value = decodeURIComponent(pair[1]); // 解码值  
    params[key] = value; // 存储键值对  
    }  
    return params;
}
const MockData = [
    {
        id: 1,
        name: 'ljm',
        age: 18,
    },
    {
        id: 2,
        name: 'hjl',
        age: 19,
    }
]
​
var server = http.createServer(function (req, res) {
    if(req.method == 'GET'){
        params = getParams(req.url);
        res.writeHead(200, {'Content-Type': 'text/plain'});
        const data = MockData.filter(item => item.id == params.id);
        if(data.length > 0){
            res.end(JSON.stringify(data));
        }else{
            res.end('Not Found\n');
        }
    }else{
        res.writeHead(404);
        res.end();
    }
});
server.listen(8124);

打开页面,我们发现报跨域问题了,这是因为我们的客户端和服务端不同源

下面,我们将配置代理服务器为我们转发请求:

安装代理插件:

css 复制代码
npm install http-proxy-middleware --save

配置代理:

src目录下建立文件setupProxy.js

js 复制代码
const { createProxyMiddleware } = require('http-proxy-middleware');  
  
module.exports = function(app) {  
  app.use(  
    '/api',  
    createProxyMiddleware({  
      target: 'http://127.0.0.1:8124',  
      changeOrigin: true, // 如果目标服务器不在同一个域下,需要设置为true  
      pathRewrite: {  
        '^/api': '' // 移除请求路径中的/api前缀  
      }  
    })  
  );  
};

客户端:

jsx 复制代码
import logo from './logo.svg';
import './App.css';
import axios from 'axios';
​
function App() {
    axios.get('/api/getData',{
      params:{
        id:1,
      }
    }).catch(e=>{
      console.log("error",e);
    });
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.js</code> and save to reload.
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
  );
}
​
export default App;

打开页面network,我们会发现本次请求成功了,这个url是转发前的url

上述的这种代理方式,常用于本地开发。这些配置文件并不会被一起打包部署,所以生产环境并不会使用这种方式。

生产环境下,一般会使用Nginx反向代理,其原理与本地开代理服务器类似,但是本地开发用的是正向代理

CORS

CORS是什么

我们来看MDN上的解释

跨源资源共享CORS,或通俗地译为跨域资源共享)是一种基于 HTTP 头的机制,该机制通过允许服务器标示除了它自己以外的其他(域、协议或端口),使得浏览器允许这些源访问加载自己的资源。跨源资源共享还通过一种机制来检查服务器是否会允许要发送的真实请求,该机制通过浏览器发起一个到服务器托管的跨源资源的"预检"请求。在预检中,浏览器发送的头中标示有 HTTP 方法和真实请求中会用到的头。

简单来说,就是服务器在HTTP头上配置了允许访问的源信息,当服务器响应请求后,浏览器会检查响应中的源信息是否包含前页面,如果包含,则允许跨域。

另外针对非简单请求,CORS还设置了预检机制。

简单请求与非简单请求

简单请求

简单请求不会触发CORS预检,会直接请求服务端。

简单来讲,简单请求就是使用GETHEAD以及POST发起的请求。

如果想要深入了解简单请求的满足条件,可以查看MDN官网的解释。

非简单请求

非简单请求会触发预检机制。

非简单请求指的是对服务器有特殊要求的请求。这些请求不满足简单请求的两大条件。具体而言,非简单请求包括以下几种情况:

  1. 请求方法不是HEAD、GET或POST。例如,当请求方法是PUT或DELETE时,它就被视为非简单请求。
  2. 请求的Content-Type字段的类型不是常见的类型,如application/x-www-form-urlencodedmultipart/form-datatext/plain。特别是当Content-Type字段的类型是application/json时,它也被视为非简单请求。

预检机制

非简单请求的CORS(跨源资源共享)请求在正式通信之前,会使用OPTIONS方法增加一次HTTP查询请求,这被称为"预检"请求(preflight)。浏览器会先向服务器发送这个预检请求,询问当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到服务器的肯定答复后,浏览器才会发出正式的XMLHttpRequest请求;否则,就会报错。

简单请求的发起过程:

非简单请求的发起过程:

这么设计的原因?

减轻服务器压力。不管是否会产生跨域,服务端都会返回信息,对服务端会产生较大的压力。所以通过预检机制,服务端就可以不必对会产生跨域的客户端发送数据。

简单请求适用于数据量小的,需要高响应速度的场景。

非简单请求则适用于数据量较大的场景,减轻服务器的压力。

如何配置CORS解决跨域

通过前面的讲解,我们知道想要实现CORS,需要以下两个条件:

  1. 服务端配置正确的源信息到http请求头上
  2. 浏览器必须实现CORS相关规范

还是从3000端口起一个服务,这次我们不经过代理服务器直接向8124端口发起请求

js 复制代码
axios.get('http://localhost:8124',{
  params:{
    id:1,
  }
}).catch(e=>{
  console.log("error",e);
});

这里我们使用的是chorme浏览器,服务端还未配置源信息,所以发生了跨域报错。

配置服务端响应头:

js 复制代码
var http = require('http');
function getParams(url) {
    const queryString = url.split('?')[1] ?? ''; // 获取查询字符串部分  
    const pairs = queryString?.split('&'); // 将字符串按&分隔成键值对数组  
    const params = {};  
    for (let i = 0; i < pairs.length; i++) {  
    const pair = pairs[i].split('='); // 将键值对按=分隔  
    const key = decodeURIComponent(pair[0]); // 解码键  
    const value = decodeURIComponent(pair[1]); // 解码值  
    params[key] = value; // 存储键值对  
    }  
    return params;
}
const MockData = [
    {
        id: 1,
        name: 'ljm',
        age: 18,
    },
    {
        id: 2,
        name: 'hjl',
        age: 19,
    }
]
​
var server = http.createServer(function (req, res) {
    console.log(req.url);
    res.setHeader('Access-Control-Allow-Credentials', true); // 允许后端发送cookie
    res.setHeader('Access-Control-Allow-Origin', 'http://localhost:3000'); // 允许访问的域名
    res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With,Content-Type'); // 允许的请求头
    res.setHeader('Access-Control-Allow-Methods', 'PUT,POST,GET,DELETE,OPTIONS'); // 允许的请求方法
    if(req.method == 'GET'){
        params = getParams(req.url);
        res.writeHead(200, {'Content-Type': 'text/plain'});
        const data = MockData.filter(item => item.id == params.id);
        if(data.length > 0){
            res.end(JSON.stringify(data));
        }else{
            res.end('Not Found\n');
        }
    }else{
        res.writeHead(404);
        res.end();
    }
});
server.listen(8124);

重启服务,客户端重新发送请求

请求成功,从回调的头中,我们可以看到我们配置的CORS信息

总结

跨域问题的产生是因为浏览器的同源策略,同源策略的产生也是由于安全问题。但有时候,我们必须要跨域,我们可以通过如下方式解决跨域:

方式 原理 适用场景
JSONP 利用script不受跨域机制的限制发送请求 只能发送get请求,并且需要前后端配合,实际开发不推荐使用
代理服务器 起一个同源服务器,利用同源服务器代为转发请求 本地开发的时候比较方便,生产环境下用nignx或者apache等作为容器进行转发,比较强大。
CORS 浏览器机制,服务端配置响应头CORS信息 比较简单,只需要服务端配置好允许访问的源即可。
相关推荐
2401_8827275714 分钟前
低代码配置式组态软件-BY组态
前端·后端·物联网·低代码·前端框架
NoneCoder17 分钟前
CSS系列(36)-- Containment详解
前端·css
anyup_前端梦工厂28 分钟前
初始 ShellJS:一个 Node.js 命令行工具集合
前端·javascript·node.js
5hand33 分钟前
Element-ui的使用教程 基于HBuilder X
前端·javascript·vue.js·elementui
GDAL1 小时前
vue3入门教程:ref能否完全替代reactive?
前端·javascript·vue.js
六卿1 小时前
react防止页面崩溃
前端·react.js·前端框架
z千鑫1 小时前
【前端】详解前端三大主流框架:React、Vue与Angular的比较与选择
前端·vue.js·react.js
m0_748256142 小时前
前端 MYTED单篇TED词汇学习功能优化
前端·学习
小白学前端6663 小时前
React Router 深入指南:从入门到进阶
前端·react.js·react
web130933203983 小时前
前端下载后端文件流,文件可以下载,但是打不开,显示“文件已损坏”的问题分析与解决方案
前端