【案例】同源策略 - CORS 处理

相信,作为一个前端开发者,我们都会遇到下面的问题:

那么,这种 CORS 问题,是怎么引起的呢?

CORS 是什么

CORS 全称 Cross Origin Resource Sharing,即 "跨域资源共享"

CORS 是一种浏览器安全机制,用于控制跨域资源的访问。 它允许浏览器向跨源服务器,发出 XMLHttpRequest 请求,从而克服了 AJAX 只能同源使用的限制。

XMLHttpRequestJavaScript 中的内置对象,用于浏览器中发送 HTTP 请求并与服务器进行通信。

整个 CORS 通信过程,都是浏览器自动完成,不需要用户参与。

当浏览器直接发出 CORS 请求,会在头信息中,增加一个 Origin 字段。

什么是同源

我们知道 CORS 是因为不同源产生的。那么,同源,需要具备什么条件呢?

同源策略是一种重要的安全策略 。同源(Same Origin)是指两个 URL 的协议、主机和端口号都相同,即满足下面的条件:

  1. 协议(Protocol):URL 的协议部分,比如 HTTP、HTTPS 必须完全相同。
  2. 主机(Host):URL 的主机部分,即域名或者 IP 地址,必须完全相同。
  3. 端口号(Port):URL 的端口部分必须完全相同。

我们以 URL - http://www.a.example.com:5500/index 为参考,则有:

比较地址 是否同源 理由
https://www.a.example.com:5500/home 不同源 协议不同
http://www.b.example.com:5500/about 不同源 主机不同
http://www.a.example.com:3000/product 不同源 端口号不同
http://www.a.example.com:5500/company 同源 协议、主机和端口号都相同

浏览器为什么需要同源策略

那么,浏览器为什么需要同源策略呢,自动默认允许跨域获取不就行了?

一言以蔽之:为了用户的安全。防止恶意网站通过跨域请求,来获取用户的敏感信息或进行恶意操作。浏览器同源策略限制了网页或者脚本对其他域下的资源进行读取和修改,从而保护了用户的隐私和安全。

假设浏览器没有同源策略,你登陆了银行站点,然后在第三方平台能够获取获你银行的密码:

使用 CORS 处理跨源问题

开篇,我们看到了因为不同源,浏览器在请求后端接口的时候报了错误。接下来,我们将使用 CORS 来允许跨源访问。

我们通过案例来实践下:

案例的演示环境:

macOS Monterey - Apple M1

node version - v14.18.1

Visual Studio Code 及其 Live Server 插件

首先,我们添加个 hostname, 方便测试,当然你可以直接使用 ip 地址测试。

通过 sudo vim /etc/hosts 添加 127.0.0.1 a.example.com 的映射:

我们简单生成客户端网页内容:

html 复制代码
<!-- index.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>
  <button id="trigger">请求接口</button>
  <p>下面是接口返回信息:</p>
  <div id="dataOutput"></div>

  <script>
    (function() {
      document.getElementById("trigger").addEventListener("click", function() {
        fetch('http://a.example.com:3000', {
          method: 'GET'
        })
        .then(response => response.text())
        .then(data => {
          document.getElementById('dataOutput').innerText = data;
        })
        .catch(error => console.error(error));
      })
    })()
    
  </script>
</body>
</html>

上面的代码很简单,我们编写了一个 请求接口 的按钮,然后,点击按钮,会触发 GET 接口请求,最后将返回的内容在页面中展示出来。

启动项目后,可以访问 http://a.example.com:5500/ 查看页面效果:

端口号是 Live Server 插件生成的,项目运行起来后,读者可以留意下 IDE 右下角的 Port

此时点击请求接口按钮的话,会报错。因为我们还没开启服务。

下面👇,我们使用 node 简单编写后端服务:

javascript 复制代码
// index.js
const Koa = require('koa');
const app = new Koa();

app.use(async ctx => {
  ctx.response.body = {
    message: 'Hello! Jimmy.'
  }
})

app.listen(3000, () => {
  console.log("Server is running on port 3000");
})

上面,我们引入了 Web 的开发框架 Koa,在根路由返回了 { message: 'Hello! Jimmy.' } 的信息,然后监听 3000 端口。启动服务后,我们访问 http://a.example.com:3000/,就会看到下面的信息:

哎呦,不错哦~

服务已经起来了,我们再次点击下请求接口按钮,此时还是会报错 CORS

OK~ 我们在服务端为 Access-Control-Allow-Origin 添加通配符 * 告诉浏览器,你需要允许所有针对向我发起的请求开绿灯

javascript 复制代码
// index.js
app.use(async ctx => {
  ctx.set('Access-Control-Allow-Origin', '*'); // + 允许跨域
  ctx.response.body = {
    message: 'Hello! Jimmy.'
  }
})

Access-Control-Allow-Origin :必须字段,它的值是要么是请求时 Origin 字段的值,要么是一个 *,表示接受任何域名的请求。

我们推荐使用 Origin 处理,避免黑客攻击。上面的 Origin 可写成 ctx.set('Access-Control-Allow-Origin', 'http://a.example.com:5500');

那么,我们可以编写多个 Origin?不然,一个服务就允许一个域名访问嘛,而且使用 * 又不推荐。

是的,我们可以处理多个 Origin,配置下白名单即可,如下:

javascript 复制代码
// index.js
// + 配置白名单
const originArray = [
  'http://a.example.com:5000',
  'http://a.example.com:5500'
];

app.use(async ctx => {
  // ctx.set('Access-Control-Allow-Origin', '*'); // -
  const { origin } = ctx.request.header; // + 获取请求源
  ctx.set('Access-Control-Allow-Origin', originArray.includes(origin) ? origin : null); // + 设置允许的源
  ctx.response.body = {
    message: 'Hello! Jimmy.'
  }
})

上面,我们设置了白名单,然后在响应中比较请求的 origin 并写入允许的内容。

延伸阅读 - 处理跨源访问的方法

处理 CORS 问题,除了在服务端添加 Access-Control-Allow-Origin 响应头内容的做法,我们还有其他处理方案?

答案是,有的

比如:

1. 代理服务器

验证环境在 amazon-linux-ami,我们更改站点请求 fetch 接口。

这里以 nginx 作为代理服务器。

bash 复制代码
// custom_name.conf
# service
location /api {
  proxy_pass http://api;
  # 隐藏原先服务端配置的头信息
  proxy_hide_header Access-Control-Allow-Origin;
  # 添加允许的地址
  add_header Access-Control-Allow-Origin http://a.example.com:5500; 
}

2. JSONP

JSONP(JSON with Padding) 是一种用于解决浏览器的同源策略限制的跨域数据请求方法。

它利用了 <script> 标签跨域的特性,配合后端来完成。

JSONP 的使用步骤如下:

  1. 前端定义一个回调函数,通常以指定的名称命名。
  2. 向后端发起一个以该回调函数名作为查询参数的请求。
  3. 后端接收到请求后,将数据包装在回调函数中并返回给前端。
  4. 前端接收到返回的数据后,会自动执行回调函数,并获得数据进行处理。

下面以 callback=handleResponsequery 值的一个简单例子:

我们更改下 index.html 文档:

html 复制代码
<!-- index.html -->
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>jsonp 跨域测试</title>
</head>
<body>
  <button id="trigger">请求接口</button>
  <p>下面是接口返回信息:</p>
  <div id="dataOutput"></div>
  <script defer>
    function handleResponse(data) {
      document.getElementById('dataOutput').innerText = JSON.stringify(data);
    }

    document.getElementById("trigger").addEventListener("click", function() {
      let script = document.createElement('script');
      script.src = 'http://a.example.com:3000/?callback=handleResponse';
      document.getElementsByTagName('head')[0].appendChild(script);
    })
  </script>
</body>
</html>

上面,我们点击按钮 请求接口 会生成一个带 http://a.example.com:3000/?callback=handleResponse 链接的 script 标签,然后处理拉取回来的资源信息,并将信息展示在页面。

我们接下来简单书写下后端服务,在原先的 index.js 文件上进行更改:

javascript 复制代码
// index.js
app.use(async ctx => {
  // ctx.response.body = {
  //   message: 'Hello! Jimmy.'
  // }
  const callback = ctx.query.callback; // 获取毁掉函数名
  ctx.type = 'text/javascript'; // 设置返回内容的类型为 JavaScript
  ctx.body = callback + '(' + JSON.stringify({
    message: 'Hello! Jimmy.'
  }) + ')'; // 返回数据
})

不推荐使用 JSONP 来处理跨域问题,它具有一定的局限性:只支持 GET 请求,并且由于安全性考虑以及数据传输的大小限制,它一般适用于获取非敏感的公开数据。

3. WebSocket

WebSocket 是一种在网络上建立双向实时通信的协议。通常用在股票等实时 要求的场景。WebSocket 的连接不受同源策略的限制,可以跨域通信。

下面是一个简单的例子,模拟 webscocket 的实时通信:

html 复制代码
<!-- index.html -->
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>WebSocket 跨域测试</title>
</head>
<body>
  <button id="trigger">请求接口</button>
  <p>下面是接口返回信息:</p>
  <div id="dataOutput"></div>
  <script>
    (function() {
      document.getElementById("trigger").addEventListener("click", function() {
        // 创建 socket 链接
        const socket = new WebSocket('ws://a.example.com:3000');
        // 连接成功触发
        socket.onopen = function() {
          socket.send('Hello! Ivy.')
        }
        // 接收到消息触发
        socket.onmessage = function(event) {
          const message = event.data;
          document.getElementById('dataOutput').innerText = message;
          console.log('接收到来自服务器的消息:', message);
        }
        // 连接关闭时触发
        socket.onclose = function(event) {
          console.log('WebSocket 连接已关闭');
        };

        // 处理错误情况
        socket.onerror = function(error) {
          console.error('WebSocket 错误:', error);
        };

        // 关闭WebSocket连接
        function closeWebSocket() {
          socket.close();
        }
      })
    })();
  </script>
</body>
</html>

首先网页通过按钮 请求接口 触发,当接收到服务端返回的信息后,将信息写入到页面。

后端服务,我们还是在 index.js 文件中处理,我们还引入了 socket 库处理,完整的代码如下:

javascript 复制代码
index.js
**const Koa = require('koa');
const app = new Koa();

const WebSocket = require('ws');
const WebSocketServer = WebSocket.Server;

// 创建应用程序,并将其传递给 WebSocket 服务器
const server = require('http').createServer(app.callback());
const wss = new WebSocketServer({server});

// 监听事件
wss.on('connection', function(ws) {
   // 监听消息事件
   ws.on('message', function (message) {
    const text = message.toString('utf-8');
    console.log('收到客户端消息:', text);
  });

  // 发送消息给客户端,模拟推送
  let i = 1;
  const interval = setInterval(() => {
    ws.send('Hello! Jimmy. No.'+i);
    i += 1;
  }, 1000);

  // 监听关闭事件
  ws.on('close', function () {
    console.log('WebSocket连接已关闭');
  });
})

server.listen(3000, () => {
  console.log("Server is running on port 3000");
})

我们监听来自客户端的信息,这里接收到的数据是二进制,所以转为文本显示。然后通过 setInterval 模拟 socket 实时推送的功能。最终效果如图:

WebSocket 方式,只是当做了解获取服务端跨域资源的知识点。仅适用实时 要求的场景。如果是平常的需求,请使用 CORS 处理跨域,并使用轮询的方式进行。PS:掘金的消息通知,使用的应该就是轮训~

参考

相关推荐
阿伟来咯~20 分钟前
记录学习react的一些内容
javascript·学习·react.js
吕彬-前端25 分钟前
使用vite+react+ts+Ant Design开发后台管理项目(五)
前端·javascript·react.js
学前端的小朱28 分钟前
Redux的简介及其在React中的应用
前端·javascript·react.js·redux·store
许野平35 分钟前
Rust: 利用 chrono 库实现日期和字符串互相转换
开发语言·后端·rust·字符串·转换·日期·chrono
guai_guai_guai37 分钟前
uniapp
前端·javascript·vue.js·uni-app
也无晴也无风雨38 分钟前
在JS中, 0 == [0] 吗
开发语言·javascript
bysking2 小时前
【前端-组件】定义行分组的表格表单实现-bysking
前端·react.js
王哲晓2 小时前
第三十章 章节练习商品列表组件封装
前端·javascript·vue.js
fg_4112 小时前
无网络安装ionic和运行
前端·npm
理想不理想v2 小时前
‌Vue 3相比Vue 2的主要改进‌?
前端·javascript·vue.js·面试