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

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

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

作者:无限大

推荐阅读时间:10 分钟

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

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

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

📜 访客通行证 = 同源策略

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

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


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

🧐 什么是同源策略?

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

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

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

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

💡 为什么需要同源策略?

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

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

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

攻击原理 :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):身份冒用的武器

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

技术实现

html 复制代码
<!-- 恶意网站隐藏表单 -->
<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 头部错误(最常见)
csharp 复制代码
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. 凭据不允许错误
csharp 复制代码
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. 方法不允许错误
csharp 复制代码
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 之前进行了拦截检查。这个过程包含三个关键步骤:

浏览器拦截流程示意图
flowchart TD A[前端发送跨域请求] --> B{是否为简单请求?} B -->|是| C[直接发送请求并添加Origin头] B -->|否| D[先发送预检请求OPTIONS] D --> E[服务器验证预检请求] E -->|验证失败| F[返回错误响应] E -->|验证通过| C C --> G[服务器处理请求并返回响应] G --> H[浏览器检查CORS响应头] H -->|检查通过| I[将响应数据交给前端JavaScript] H -->|检查失败| J[拦截响应并在控制台抛出CORS错误] F --> J classDef success fill:#90EE90,stroke:#333,stroke-width:2px classDef danger fill:#FFA07A,stroke:#333,stroke-width:2px class I success class F,J danger
浏览器拦截的三步流程
  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 实现

javascript 复制代码
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");
  // 允许携带Cookie
  res.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 配置:

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;

        # 预检请求直接返回204
        if ($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 实现代码示例

前端实现:

javascript 复制代码
// 创建回调函数
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):

javascript 复制代码
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 取代,但在需要兼容极低版本浏览器的场景仍有使用价值。


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

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

🔧 工作原理

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

flowchart LR subgraph 浏览器 A[前端应用] -->|同域请求| B[代理服务器] end B -->|转发请求| C[目标服务器] C -->|响应数据| B B -->|返回数据| A style A fill:#f9f,stroke:#333 style B fill:#9f9,stroke:#333 style C fill:#99f,stroke:#333
  1. 前端将请求发送到同域代理服务器

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

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

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

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

💻 开发环境代理配置

Vite 配置:

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

Webpack 配置:

javascript 复制代码
// webpack.config.js
module.exports = {
  devServer: {
 proxy:
      '/api': {
        target: 'http://api.example.com',
        changeOrigin: true,
        pathRewrite: {'^/api': ''}
      }
    }
  }
🚀 生产环境代理(Nginx)
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 实现代码

前端实现:

javascript 复制代码
// 创建 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 库):

javascript 复制代码
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.hash iframe 通信 利用 URL 哈希值传递数据 兼容性好但数据量有限
window.name iframe 通信 利用 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 (自定义头)
🔍 简单请求完整示例

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

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

以下请求会触发预检:

javascript 复制代码
// 带自定义头的POST请求(会触发预检)
fetch("https://api.example.com/data", {
  method: "POST",
  headers: {
    "Content-Type": "application/json", // 非简单Content-Type
    Authorization: "Bearer token123", // 非简单请求头
    "X-User-ID": "12345", // 自定义头
  },
  body: JSON.stringify({ name: "跨域请求" }),
});
预检请求/响应流程:
  1. 预检请求(OPTIONS):浏览器自动发送
makefile 复制代码
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. 预检响应:服务器返回
yaml 复制代码
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 开发中的一个挑战,但也是提升应用安全性和用户体验的重要环节,好好利用可以让你的应用程序更加高效。😉

相关推荐
晨岳29 分钟前
web开发-CSS/JS
前端·javascript·css
冲!!1 小时前
前端获取当前日期并格式化(JS)
开发语言·前端·javascript
雲墨款哥1 小时前
算法练习-Day1-交替合并字符串
javascript·算法
福娃B1 小时前
【React】React初体验--手把手教你写一个自己的React初始项目
前端·javascript·react.js
油丶酸萝卜别吃2 小时前
怎么判断一个对象是不是vue的实例
前端·javascript·vue.js
Mintopia3 小时前
Three.js 滚动条 3D 视差动画原理解析
前端·javascript·three.js
蓝乐4 小时前
Angular项目IOS16.1.1设备页面空白问题
前端·javascript·angular.js
today喝咖啡了吗4 小时前
uniapp 动态控制横屏(APP 端)
前端·javascript·uni-app
Web极客码4 小时前
如何在服务器上获取Linux目录大小
linux·服务器·javascript
sunbyte4 小时前
50天50个小项目 (Vue3 + Tailwindcss V4) ✨ | NotesApp(便签笔记组件)
前端·javascript·css·vue.js·笔记·tailwindcss