相信,作为一个前端开发者,我们都会遇到下面的问题:
那么,这种 CORS
问题,是怎么引起的呢?
CORS 是什么
CORS
全称 Cross Origin Resource Sharing
,即 "跨域资源共享"。
CORS
是一种浏览器安全机制,用于控制跨域资源的访问。 它允许浏览器向跨源服务器,发出 XMLHttpRequest
请求,从而克服了 AJAX
只能同源使用的限制。
XMLHttpRequest
是JavaScript
中的内置对象,用于浏览器中发送HTTP
请求并与服务器进行通信。
整个 CORS
通信过程,都是浏览器自动完成,不需要用户参与。
当浏览器直接发出 CORS
请求,会在头信息中,增加一个 Origin
字段。
什么是同源
我们知道 CORS
是因为不同源产生的。那么,同源,需要具备什么条件呢?
同源策略是一种重要的安全策略 。同源(Same Origin
)是指两个 URL
的协议、主机和端口号都相同,即满足下面的条件:
- 协议(
Protocol
):URL
的协议部分,比如HTTP、HTTPS
必须完全相同。 - 主机(
Host
):URL
的主机部分,即域名或者IP
地址,必须完全相同。 - 端口号(
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
的使用步骤如下:
- 前端定义一个回调函数,通常以指定的名称命名。
- 向后端发起一个以该回调函数名作为查询参数的请求。
- 后端接收到请求后,将数据包装在回调函数中并返回给前端。
- 前端接收到返回的数据后,会自动执行回调函数,并获得数据进行处理。
下面以 callback=handleResponse
为 query
值的一个简单例子:
我们更改下 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:掘金的消息通知,使用的应该就是轮训~