当前位置: 首页 > news >正文

计算机“十万个为什么”之跨域

计算机“十万个为什么”之跨域

本文是计算机“十万个为什么”系列的第五篇,主要是介绍跨域的相关知识。

作者:无限大

推荐阅读时间:10 分钟

一、引言:为什么会有跨域这个“拦路虎”?

想象你正在参观一座戒备森严的城堡 🏰

🚪 城堡大门 = 浏览器安全机制

📜 访客通行证 = 同源策略

🔄 没有通行证却想进入其他城堡的访客 = 跨域请求

在 Web 世界中,跨域就像城堡之间的访问限制,是浏览器为保护用户数据安全而设置的重要防线。但为什么需要这样的限制?当我们访问不同网站时到底发生了什么?这篇文章将带你深入探索跨域的奥秘,从基础概念到高级解决方案,全面理解这个 Web 开发中不可避免的技术挑战。


二、跨域的本质:浏览器的“安全守门人”

🧐 什么是同源策略?

同源策略(Same-Origin Policy) 是浏览器实施的核心安全策略,它要求网页只能请求与其自身协议、域名、端口完全相同的资源。这就像现实生活中,你家的钥匙只能打开你家的门,不能打开邻居家的门一样,是一种最基本的安全边界。

🔍 同源判断标准(三要素)
要素说明示例
协议通信协议必须相同httphttps 不同
域名主域名和子域名都必须相同www.example.comapi.example.com 不同
端口网络端口号必须相同808080 不同

注意:IE 浏览器在判断同源时存在例外,它不检查端口,并且允许主域名相同的不同子域之间通信。这是历史遗留问题,现代浏览器已修复此行为。

🚫 典型跨域场景示例
当前页面 URL请求资源 URL是否跨域原因
http://www.example.comhttps://www.example.com/api✅ 是协议不同 (http vs https)
http://www.example.comhttp://www.baidu.com✅ 是域名不同
http://www.example.com:80http://www.example.com:8080✅ 是端口不同
http://www.example.comhttp://api.example.com✅ 是子域名不同
http://www.example.comhttp://www.example.com/path❌ 否完全同源

💡 为什么需要同源策略?

同源策略看似“限制重重”,实则是保护用户安全的重要屏障。它通过严格的边界控制,构建了 Web 安全的第一道防线。没有它,互联网将变成危机四伏的“狂野西部”。

🔍 没有同源策略的安全灾难

想象一个没有门禁系统的办公楼——任何人都可以自由进出任何办公室,翻阅文件柜,甚至冒充员工签署文件。同源策略正是 Web 世界的门禁系统,防止以下三类致命攻击:

1. Cookie 劫持攻击:身份盗窃的温床

攻击原理:Cookie 通常存储用户登录凭证。没有同源限制,恶意网站可通过 document.cookie直接读取其他网站的 Cookie,获取你的银行账户、邮箱、社交平台等登录状态。

真实案例:2018 年 Facebook 剑桥分析事件中,第三方应用通过获取用户 Cookie 数据,在未经许可情况下访问了 8700 万用户的个人信息。

防护机制:同源策略禁止不同源页面访问 Cookie,配合 HttpOnly属性可进一步防止 JavaScript 读取敏感 Cookie。

2. DOM 篡改攻击:视觉欺诈的陷阱

攻击原理:恶意网站可通过 JavaScript 操作其他网站的 DOM 结构,例如在银行页面上覆盖虚假的登录表单,或修改电商网站的支付金额。

典型场景:当你同时打开 yourbank.comfakebank.com时,后者可修改前者页面内容,将转账金额从 100 元改为 10000 元,而你完全无法察觉。

防护机制:同源策略禁止跨域 DOM 访问,确保每个网站的页面内容只能被自身 JavaScript 操控。

3. 跨站请求伪造(CSRF):身份冒用的武器

攻击原理:恶意网站可伪造请求,利用你已登录的身份向其他网站发送操作指令。例如,当你登录网银后访问恶意网站,它可自动发起转账请求。

技术实现

<!-- 恶意网站隐藏表单 -->
<form action="https://yourbank.com/transfer" method="POST" id="stealForm"><input type="hidden" name="toAccount" value="attackerAccount" /><input type="hidden" name="amount" value="10000" />
</form>
<script>// 自动提交表单document.getElementById("stealForm").submit();
</script>

防护机制:同源策略限制跨域请求,结合 CSRF Token、Referer 验证等机制可有效防范。


🌰 生动案例:一次未遂的银行抢劫

假设你同时打开了两个标签页:

  • https://yourbank.com(已登录网银)
  • https://malicious.com(恶意网站)

没有同源策略时:

  1. 恶意网站读取你银行页面的 Cookie,获取登录状态
  2. 修改银行页面 DOM,添加隐藏转账表单
  3. 自动提交表单,将你的资金转移到攻击者账户

同源策略如何防护:

✅ 阻止读取银行 Cookie

✅ 禁止修改银行页面 DOM

✅ 限制跨域请求发送

这就是为什么浏览器会严格执行同源策略——它不是技术限制,而是保护你数字财产的安全卫士。


三、跨域的表现:浏览器如何“拦截”请求?

很多开发者第一次遇到跨域问题时都会感到困惑:明明网络请求成功了,服务器也返回了数据,为什么前端就是拿不到?要理解这个问题,我们需要深入了解浏览器拦截跨域请求的完整流程和技术细节。

  • 网络面板:显示真实的请求和响应状态(如 200 OK),因为这是服务器实际返回的状态
  • 控制台:显示 CORS 错误,因为浏览器拦截了响应,前端无法访问数据

🔍 跨域错误的典型表现

当跨域请求被浏览器拦截时,控制台会出现类似以下的错误信息(不同浏览器措辞略有差异):

常见错误类型及示例
  1. 缺少 CORS 头部错误(最常见)
Access to fetch at 'http://api.example.com/data' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
  1. 凭据不允许错误
Access to fetch at 'http://api.example.com/data' from origin 'http://localhost:3000' has been blocked by CORS policy: The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'.
  1. 方法不允许错误
Access to fetch at 'http://api.example.com/data' from origin 'http://localhost:3000' has been blocked by CORS policy: Method PUT is not allowed by Access-Control-Allow-Methods in preflight response.

🕵️‍♂️ 关键真相:请求已发送,响应被拦截

重要理解:跨域请求实际已发送到服务器,服务器也已处理并返回响应,但浏览器在将响应交给前端 JavaScript 之前进行了拦截检查。这个过程包含三个关键步骤:

浏览器拦截流程示意图
验证失败
验证通过
检查通过
检查失败
前端发送跨域请求
是否为简单请求?
直接发送请求并添加Origin头
先发送预检请求OPTIONS
服务器验证预检请求
返回错误响应
服务器处理请求并返回响应
浏览器检查CORS响应头
将响应数据交给前端JavaScript
拦截响应并在控制台抛出CORS错误
浏览器拦截的三步流程
  1. 请求发送阶段

    • 浏览器允许请求发送到目标服务器
    • 自动添加 Origin 请求头标识来源
    • 对于非简单请求,先发送预检请求(OPTIONS)
  2. 服务器响应阶段

    • 服务器处理请求并返回响应
    • 若服务器未正确配置 CORS 头部,响应中会缺少必要的允许信息
    • 即使服务器返回 200 状态码,浏览器仍可能拦截响应
  3. 浏览器检查阶段

    • 浏览器检查响应中的 CORS 头部
    • 若检查不通过,丢弃响应数据并抛出控制台错误
    • 若检查通过,将响应数据交给前端 JavaScript

这就是为什么你在网络面板(Network)中能看到 200 状态码的响应,却在控制台看到 CORS 错误的原因。浏览器充当了“安全门卫”的角色,即使服务器已提供数据,也会基于安全策略决定是否将数据交给前端。


四、跨域解决方案全景:从基础到高级

面对跨域问题,开发者们探索出了多种解决方案。选择哪种方案取决于你的具体场景:

是开发环境还是生产环境?

是简单的 GET 请求还是复杂的交互?

是否有权限修改服务器配置?

🅰️ 方案一:CORS(跨域资源共享)—— 官方标准方案

CORS(Cross-Origin Resource Sharing) 通过服务器设置 HTTP 响应头来告诉浏览器允许跨域请求,是 W3C 推荐的标准解决方案。

🔧 基本原理
  1. 浏览器发送请求时自动添加 Origin头,表明请求来源
  2. 服务器返回 Access-Control-Allow-Origin等响应头,表明是否允许该来源访问 3.浏览器检查响应头,决定是否将数据交给前端
📝 核心响应头配置

CORS 通过以下关键响应头控制跨域访问权限,每个头部都有特定的用途和安全考量:

  1. Access-Control-Allow-Origin

    • 允许值:具体的源 URL(如 https://example.com)或通配符 *
    • 作用:指定允许访问资源的外部域
    • 安全约束:生产环境中应明确指定源,避免使用 *通配符;当请求需要携带凭据(如 Cookie)时,不能使用 *
    • 示例Access-Control-Allow-Origin: https://your-frontend.com
  2. Access-Control-Allow-Methods

    • 允许值:逗号分隔的 HTTP 方法列表(如 GET, POST, PUT, DELETE
    • 作用:指定允许的 HTTP 请求方法
    • 安全约束:应仅开放必要的方法,遵循最小权限原则
    • 示例Access-Control-Allow-Methods: GET, POST, PUT, DELETE
  3. Access-Control-Allow-Headers

    • 允许值:逗号分隔的请求头列表(如 Content-Type, Authorization
    • 作用:指定允许的自定义请求头
    • 注意事项:对于非简单请求头(如 Authorization),必须显式声明
    • 示例Access-Control-Allow-Headers: Content-Type, Authorization
  4. Access-Control-Allow-Credentials

    • 允许值:布尔值 true(仅当允许凭据时)
    • 作用:指示是否允许跨域请求携带凭据(如 Cookie、HTTP 认证信息)
    • 安全考量:启用此选项会增加安全风险,需确保源验证严格
    • 示例Access-Control-Allow-Credentials: true
  5. Access-Control-Max-Age

    • 允许值:正整数(单位:秒)
    • 作用:指定预检请求(OPTIONS)结果的缓存时间
    • 优化建议:合理设置缓存时间(如 86400 秒=24 小时)可减少预检请求次数
    • 示例Access-Control-Max-Age: 86400

这些响应头需要配合使用,共同构成完整的 CORS 安全策略。服务器必须正确配置这些头部才能使跨域请求正常工作。

💻 CORS 实现代码示例

Node.js/Express 实现

const express = require("express");
const app = express();// 全局CORS中间件
app.use((req, res, next) => {// 允许指定源访问res.setHeader("Access-Control-Allow-Origin", "https://your-frontend.com");// 允许的方法res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE");// 允许的请求头res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");// 允许携带Cookieres.setHeader("Access-Control-Allow-Credentials", "true");// 处理预检请求if (req.method === "OPTIONS") {res.statusCode = 204; // 预检请求不需要响应体return res.end();}next();
});// API路由
app.get("/data", (req, res) => {res.json({ message: "跨域请求成功!" });
});app.listen(3000, () => {console.log("服务器运行在端口3000");
});

Nginx 配置:

server {listen       ;server_name  api.example.com;location / {# 允许跨域add_header Access-Control-$allow_origin https://example.com;add_header Access-Control-Allow-Methods GET,POST,PUT,DELETE;add_header Access-Control-Allow-Headers Content-Type,Authorization;add_header Access-Control-Allow-Credentials true;# 预检请求直接返回204if ($request_method = 'OPTIONS') {return ;}proxy_pass http://localhost:3000;}
}
⚠️ CORS 安全最佳实践
  1. 避免使用 *通配符: 在生产环境中应明确指定允许访问的源
  2. 限制允许的方法:只开放必要 HTTP 方法
  3. 谨慎启用 Credentials:允许 Cookie 跨域传输会增加安全风险
  4. 合理设置 Max-Age:减少预检请求次数提升性能

🅱️ 方案二:JSONP —— 古老但仍在使用的技巧

JSONP (JSON with Padding) 是一种利用 <script>标签不受同源策略限制特性的跨域方案,虽然古老但在一些兼容性要求高的场景仍有应用。

🔧 工作原理
  1. 前端创建 <script>标签并指定服务器 URL,附带回调函数名
  2. 服务器返回 JavaScript 代码,格式为 回调函数名(数据)
  3. 浏览器执行返回的 JavaScript,调用回调函数处理数据
💻 JSONP 实现代码示例

前端实现:

// 创建回调函数
function handleResponse(data) {console.log("JSONP 返回数据:", data);
}// 动态创建 script 标签
function fetchDataWithJSONP() {const script = document.createElement("script");// 传递回调函数名给服务器script.src = "http://api.example.com/data?callback=handleResponse";document.body.appendChild(script);// 使用后移除 script 标签script.onload = () => {document.body.removeChild(script);};
}// 调用函数发起请求
fetchDataWithJSONP();

服务器实现(Node.js):

const http = require('http');
const url = require('url');const server = http.createServer((req,const query = url.parse(req.url,const callback = query.callback;const data = JSON.stringify({ message: 'JSONP请求成功' });// 返回JavaScript代码,调用回调函数res.writeHead(res.end(`${callback}(${data})`);
});server.listen(3000);
⚠️ JSONP 的局限性

1.仅支持 GET 请求:无法发送 POST 等复杂请求

2.安全风险:可能遭受 XSS 攻击

3.错误处理困难:缺乏标准的错误处理机制

4.无法设置请求头:难以实现认证等功能

JSONP 已逐渐被 CORS 取代,但在需要兼容极低版本浏览器的场景仍有使用价值。


🅲️ 方案三: 代理服务器 —— 前端无感方案

代理服务器通过在同域服务器端转发请求来绕过浏览器同源限制,是开发环境中最常用方案之一。

🔧 工作原理

代理服务器充当中间人,将跨域请求转发到目标服务器,前端只与同域代理服务器通信,浏览器不会触发跨域限制。

浏览器
同域请求
转发请求
响应数据
返回数据
代理服务器
前端应用
目标服务器
  1. 前端将请求发送到同域代理服务器

  2. 代理服务器转发请求到目标服务器

  3. 目标服务器返回响应给代理服务器

  4. 代理服务器将响应返回给前端

由于前端只与同域代理服务器通信浏览器不会触发跨域限制

💻 开发环境代理配置

Vite 配置:

// vite.config.js
export default {server: {proxy: {"/api": {target: "http://api.example.com", //目标服务器changeOrigin: true, // 更改请求源rewrite: (path) => path.replace(/^\/api/, ""), // 可选重写路径},},},
};

Webpack 配置:

// webpack.config.js
module.exports = {devServer: {proxy:'/api': {target: 'http://api.example.com',changeOrigin: true,pathRewrite: {'^/api': ''}}}}
🚀 生产环境代理(Nginx)
server {listen       ;server_name  example.com;# 静态资源location / {root   /usr/share/nginx/html;index  index.html;}# API代理location /api/ {proxy_pass http://api.example.com/; #转发到目标服务器proxy_set_header Host $host;proxy_set_header X-Real-IP $remote_addr;}
}
⚠️ 代理服务器的局限性
  • 开发环境依赖:需要配置代理服务器,生产环境可能不适用
  • 性能开销:增加了请求转发的延迟
  • 安全风险:代理服务器可能成为攻击目标,需加强安全配置
  • 跨域限制:仍需服务器端配合,无法完全解决跨域问题

🅳️ 方案四:WebSocket ——实时通信跨域方案

WebSocket 协议是 HTML5 引入的全双工通信协议它不受同源策略限制,特别适合实时通信场景。

🔧 工作原理

WebSocket 通过一次握手建立持久连接之后的通信不再受同源策略限制。

💻 WebSocket 实现代码

前端实现:

// 创建 WebSocket 连接
const socket = new WebSocket('ws://api.example.com/chat');// 连接建立时触发
socket.addEventListener('open', (event) => {console.log('WebSocket 连接已建立');socket.send('Hello Server!'); // 发送消息
});// 接收服务器消息
socket.addEventListener('message', (event) => {console.log('收到消息:', event.data);
});// 连接关闭时触发
socket.addEventListener('close', (event) => {console.log('WebSocket 连接已关闭');
});// 发生错误时触发
socket.addEventListener('error', (event) => {console.error('WebSocket 错误:', event);
});

服务器实现(Node.js with ws 库):

const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });// 监听连接
wss.on('connection', (ws) => {console.log('新客户端连接');// 接收客户端消息ws.on('message', (message) => {console.log('收到:', message.toString());ws.send('服务器已收到: ' + message.toString());});// 连接关闭ws.on('close', () => {console.log('客户端已断开');});
});

🅴️ 其他跨域方案

方案适用场景原理优缺点
postMessage跨窗口/iframe 通信窗口间通过 postMessage方法传递数据灵活但仅限窗口间通信
document.domain同主域不同子域显式设置 document.domain为相同主域简单但仅限同主域场景
location.hashiframe 通信利用 URL 哈希值传递数据兼容性好但数据量有限
window.nameiframe 通信利用 window.name 属性存储数据可存储大量数据但实现复杂

五、深度解析:CORS 预检请求

很多开发者在使用 CORS 时会遇到一个困惑为什么明明只发送了一个请求,浏览器网络面板却显示两个请求?这就是 CORS 的预检请求机制在起作用。

🕵️‍♂️ 什么是预检请求?

预检请求(Preflight Request) 是浏览器在发送某些跨域请求前,先发送一个 OPTIONS方法请求到服务器,以确定服务器 是否允许实际请求。

🚦 触发预检请求的条件

当请求满足以下任一条件时浏览器会自动发送预检请求:

1. 使用非简单方法

简单方法包括:GETHEADPOST

非简单方法包括:PUTDELETECONNECTOPTIONSTRACEPATCH

2. 使用非简单请求头

简单请求头包括:

  • Accept
  • Accept-Language
  • Content-Language
  • Content-Type (仅允许值为 application/x-www-form-urlencodedmultipart/form-datatext/plain)

非简单请求头示例:

  • Authorization (认证令牌)
  • Content-Type: application/json (JSON 格式数据)
  • X-Custom-Header (自定义头)
🔍 简单请求完整示例

满足以下条件的请求不会触发预检:

// 简单GET请求示例
fetch("https://api.example.com/data", {method: "GET",headers: {Accept: "application/json","Accept-Language": "zh-CN",},
});
🔍 预检请求完整示例

以下请求会触发预检:

// 带自定义头的POST请求(会触发预检)
fetch("https://api.example.com/data", {method: "POST",headers: {"Content-Type": "application/json", // 非简单Content-TypeAuthorization: "Bearer token123", // 非简单请求头"X-User-ID": "12345", // 自定义头},body: JSON.stringify({ name: "跨域请求" }),
});
预检请求/响应流程:
  1. 预检请求(OPTIONS):浏览器自动发送
OPTIONS /data HTTP/1.1
Origin: https://example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type,Authorization,X-User-ID
  1. 预检响应:服务器返回
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Methods: POST
Access-Control-Allow-Headers: Content-Type,Authorization,X-User-ID
Access-Control-Max-Age: 86400
  1. 实际请求:预检通过后发送真实请求

⏱️ 预检请求优化

频繁的预检请求会影响性能,可通过以下方式优化:

  1. 设置合理的 Max-Age:缓存预检结果(单位:秒)
  2. 避免使用自定义头:优先使用简单请求头
  3. 合并请求:减少跨域请求次数
  4. 使用 GET 替代 POST:GET 请求通常为简单请求

六、总结

CORS 预检请求机制是为了确保跨域请求的安全性而引入的。开发者在使用 CORS 时需要注意触发预检请求的条件,以及合理配置服务器端响应头。通过合理优化预检请求,能够提升应用的性能和用户体验。

希望本文能够帮助你理解跨域的本质、同源策略的作用,以及如何通过 CORS、JSONP、代理等多种方式解决跨域问题。跨域虽然是 Web 开发中的一个挑战,但也是提升应用安全性和用户体验的重要环节,好好利用可以让你的应用程序更加高效。😉

http://www.lryc.cn/news/592675.html

相关文章:

  • c语言笔记---结构体
  • 一个简单的带TTL的LRU的C++实现
  • windows终端美化(原生配置+Oh My Posh主题美化)
  • 数据交易“命门”:删除权与收益分配的暗战漩涡
  • 《通信原理》学习笔记——第四章
  • LP-MSPM0G3507学习--05中断及管脚中断
  • 【DPDK】高性能网络测试工具Testpmd命令行使用指南
  • ELK结合机器学习模型预测
  • mysql not in 查询引发的bug问题记录
  • RV126平台NFS网络启动终极复盘报告
  • Python网络爬虫之selenium库
  • cocosCreator2.4 Android 输入法遮挡
  • Nginx配置Spring Boot集群:负载均衡+静态资源分离实战
  • 【时时三省】(C语言基础)通过指针引用字符串
  • cartorgapher的编译与运行
  • 群晖中相册管理 immich大模型的使用
  • 更适合后端宝宝的前端三件套之CSS
  • Node.js链接MySql
  • 前端笔记之 async/await 异步编程详解
  • 反射机制的登录系统
  • MyUI会员排名VcMember组件文档
  • Java并发编程痛点解析:从底层原理到实战解决方案
  • Axure RP 10 预览显示“无标题文档”的空白问题探索【护航版】
  • 【密码学】1. 引言
  • vue3引入cesium完整步骤
  • 深入Java注解:从内置到元注解与自定义实战指南
  • STM32-CAN
  • 开发避坑短篇(2):uni-app微信小程序开发‘createIndependentPlugin‘模块缺失问题分析与解决方案
  • 初探:C语言FILE结构之文件描述符与缓冲区的实现原理
  • iOS OC 图片压缩