《基于 Koa 的登录页面实战:跨域》
同源策略(Same-Origin Policy)
浏览器的一项重要安全策略。
其主要目的是防止不同源的文档或脚本之间的交互,以保护用户的隐私和安全。
同源的定义包括协议、域名和端口都相同。例如,http://example.com:80 和 https://example.com:443 不是同源,http://example.com 和 http://sub.example.com 也不是同源。
同源策略的限制主要体现在以下几个方面:
- Cookie、LocalStorage 和 IndexedDB 等存储数据无法在不同源之间共享。
- DOM 无法被不同源的脚本访问和操作。
- Ajax 请求受到限制,无法向不同源的服务器发送请求获取数据。
同源策略虽然保障了安全,但也给一些合理的跨域需求带来了挑战,因此出现了如 CORS 等解决跨域问题的技术和方法。
例如,如果一个网页来自 http://example1.com ,它里面的脚本就不能读取或修改来自 http://example2.com 的页面内容。
为什么会出现跨域?
跨域问题出现的主要原因是浏览器的同源策略(Same-Origin Policy)。
同源策略是一种安全机制,它要求浏览器中脚本的访问请求必须满足以下三个条件相同,才能被认为是同源的:
-
协议(Protocol):例如
http或https。 -
域名(Domain):包括主域名和子域名。
-
端口(Port):默认端口(如
http的 80 端口,https的 443 端口)。
如果请求不满足同源条件,就会被认为是跨域请求。
出现这种限制主要是出于安全考虑,防止以下潜在的风险:
-
防止恶意网站窃取用户在其他网站上的敏感信息,如登录凭证、个人数据等。
-
避免恶意网站修改其他网站的页面内容或行为。
例如,如果没有同源策略,一个恶意网站可以通过 JavaScript 向用户经常访问的银行网站发送请求,获取用户的账户信息,或者在用户不知情的情况下向其他网站提交有害的数据。
如何解决跨域?
常见的解决跨域的方法:
-
CORS(跨源资源共享) :这是一种在服务器端设置响应头,告知浏览器允许哪些源可以访问资源的方法。服务器通过设置
Access-Control-Allow-Origin等响应头来控制跨域请求的权限。- 原理:浏览器在发送跨域请求时会先发送一个“预检”请求(
OPTIONS方法),服务器返回允许的请求方法、头信息等,浏览器根据这些信息决定是否发送实际请求。
- 原理:浏览器在发送跨域请求时会先发送一个“预检”请求(
-
JSONP(JSON with Padding) :利用
<script>标签的跨域能力,服务器返回一个包含回调函数调用的 JavaScript 代码片段。- 原理:
<script>标签的src属性不受同源策略限制,通过动态创建<script>标签并指定跨域的 URL,服务端返回数据时包裹在指定的回调函数中,从而在前端获取到数据。
- 原理:
-
代理服务器:在前端和后端之间设置一个代理服务器,让前端向代理服务器发送请求,由代理服务器向目标服务器请求数据并返回给前端。
-
原理:对于浏览器来说,它与代理服务器之间的通信不存在跨域问题,而代理服务器与目标服务器之间的通信在同一域内。
-
CORS 解决跨域的具体示例:
const Koa = require('koa');
const cors = require('@koa/cors');
const app = new Koa();
app.use(cors({
origin: 'http://your-allowed-origin.com', // 允许的源
methods: ['GET', 'POST', 'PUT', 'DELETE'], // 允许的请求方法
allowedHeaders: ['Content-Type', 'Authorization'], // 允许的请求头
}));
app.listen(3000);
JSONP 示例
前端部分:
<!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">
<title>Document</title>
</head>
<body>
<script>
// 定义一个名为 handleData 的函数,用于接收和处理从服务器返回的数据
function handleData(data) {
console.log(data);
}
</script>
<!--
动态创建一个 <script> 标签,其 src 属性指向服务器的 URL
并携带了回调函数名 callback=handleData
浏览器会向该 URL 发送请求获取数据
-->
<script src="http://example.com/data?callback=handleData"></script>
</body>
</html>
后端部分(Node.js 和 Express 框架):
const express = require('express');
// 导入 Express 框架,用于创建服务器和处理路由
const app = express();
// 创建一个 Express 应用实例
app.get('/data', (req, res) => {
// 定义一个处理 '/data' 路径的 GET 请求的路由处理函数
const callbackName = req.query.callback;
// 从请求的查询参数中获取名为 'callback' 的值,并将其赋值给 callbackName
const data = { message: 'Hello from the server!' };
// 定义要返回给前端的数据对象
const script = `${callbackName}(${JSON.stringify(data)})`;
// 构建包含回调函数调用和数据的 JavaScript 脚本字符串
res.send(script);
// 将构建好的脚本字符串发送给前端
});
app.listen(3000, () => {
// 启动服务器,监听 3000 端口
console.log('Server is running on port 3000');
});
代理服务器示例(使用 Node.js 的 http-proxy-middleware 库)
const express = require('express');
// 导入 Express 框架,用于创建服务器和处理路由等功能
const { createProxyMiddleware } = require('http-proxy-middleware');
// 从 'http-proxy-middleware' 库中导入创建代理中间件的相关功能
const app = express();
// 创建一个 Express 应用实例
app.use('/api', createProxyMiddleware({
// 为 '/api' 路径设置代理中间件
target: 'http://target-server.com',
// 定义代理的目标服务器地址
changeOrigin: true,
// 允许更改请求的源信息,以模拟同源请求
onProxyRes: (proxyRes, req, res) => {
// 定义当接收到目标服务器响应时的处理函数
// 处理状态码
if (proxyRes.statusCode === 500) {
// 如果目标服务器返回的状态码是 500
proxyRes.statusCode = 502;
// 将状态码修改为 502
res.end('服务暂时不可用,请稍后再试');
// 结束响应,并向客户端发送自定义的错误消息
return;
// 结束当前处理逻辑
}
// 处理头信息
delete proxyRes.headers['Server'];
// 从响应头中删除 'Server' 字段
proxyRes.headers['X-Proxy-Added'] = 'This header is added by the proxy';
// 向响应头添加自定义字段
// 处理主体内容(假设将文本转换为大写)
let body = '';
// 初始化一个空字符串用于存储响应主体数据
proxyRes.on('data', (chunk) => {
// 当接收到响应的数据块时
body += chunk;
// 将数据块添加到 body 字符串中
});
proxyRes.on('end', () => {
// 当响应数据接收完成时
res.send(body.toUpperCase());
// 将处理后的大写主体数据发送给客户端
});
}
}));
app.listen(8080);
// 启动服务器,并监听 8080 端口