跨域

Published 2026-04-24 21:05 2920 words 15 min read

This post is not yet available in English. Showing the original.
一、跨域问题的本质:同源策略与安全边界 1.1 什么是跨域? 跨域(Cross-Origin)的核心根源,是浏览器内置的同源策略,指的是浏览器出于安全考虑,限制一个源(Origin)的脚本访问另一个源的资源。这里的"源"由三个要素组成,所谓「同源」,要求两个 URL 的以下三个要素完全一致: 协议(http vs https) 域名(example.com vs api.example.com)...

一、跨域问题的本质:同源策略与安全边界

1.1 什么是跨域?

跨域(Cross-Origin)的核心根源,是浏览器内置的同源策略,指的是浏览器出于安全考虑,限制一个源(Origin)的脚本访问另一个源的资源。这里的”源”由三个要素组成,所谓「同源」,要求两个 URL 的以下三个要素完全一致

  • 协议(http vs https)
  • 域名(example.com vs api.example.com)
  • 端口(80 vs 8080)

只要三者中任一不同,即构成跨域。举个直观的例子,以 http://www.example.com:80 为基准,同源判定结果如下:

请求 URL是否同源跨域原因
https://www.example.com协议不同(http→https)
http://api.example.com域名不同(主域一致,子域不同)
http://www.example.com:3000端口不同(80→3000)
http://www.example.com/user仅路径不同,三要素完全一致

补充:

核心定义

  • 主域(注册域名):是用户通过域名注册商合法申请、拥有完整所有权与管理权的最小独立域名单元,格式为「自定义主体 + 顶级域」,比如 example.comexample.com.cn。主域是域名所有权的核心载体,必须付费注册,可自主创建所有下级子域。
  • 子域:是主域持有者在主域基础上,免费自主配置的下级从属域名,无需额外注册。比如主域 example.com 的子域包括 www.example.comapi.example.comadmin.example.com 等,子域完全依附于主域,可独立配置解析指向不同服务,但无独立所有权。

1.2 一个必须纠正的核心误区

跨域是浏览器的单向限制,不是服务器拒绝了你的请求

真实的请求流程是这样的:

  1. 前端发起跨域请求,浏览器正常将请求发送到目标服务器
  2. 服务器接收请求,正常处理并返回响应数据
  3. 浏览器收到响应后,检查响应头的 CORS 规则,判定是否允许当前源访问
  4. 若规则不匹配,浏览器直接拦截响应,抛出 CORS 报错,前端无法拿到任何响应数据

简单说:请求发出去了,服务器也正常返回了,只是浏览器把数据「扣下了」。这也是为什么你在 Network 面板能看到请求的状态码是 200,却拿不到响应体的核心原因。

1.3 同源策略到底限制了什么?

同源策略就像浏览器给每个页面加了一道「安全隔离墙」,主要限制三类行为:

  1. 数据存储访问:禁止读取 / 修改非同源页面的 Cookie、LocalStorage、IndexedDB 等存储数据
  2. DOM 操作:禁止获取非同源页面的 DOM 元素,防止恶意页面篡改嵌入的第三方页面
  3. 网络请求:限制 XMLHttpRequest、Fetch API 发起的跨域 AJAX 请求,这也是我们最常遇到的跨域场景

但有一个关键例外:HTML 的资源嵌入标签不受同源策略限制

比如<script><img><link><iframe>等标签,可以正常加载跨域资源,这也是 JSONP、图片打点等方案的底层原理。

1.4 为什么会有同源策略?

同源策略是浏览器的核心安全机制,主要防范:

  • XSS 攻击:恶意脚本窃取用户数据

  • CSRF 攻击:伪造用户身份进行恶意操作

  • 数据泄露:敏感信息被未授权访问

  • 举两个最直观的风险场景:

    • 没有同源策略,你打开的恶意钓鱼网站,可以直接读取你网银页面的 Cookie,轻松盗取你的账户信息,发起转账操作

    • 没有同源策略,恶意页面可以嵌入你的电商支付页面,篡改 DOM 元素的支付金额,诱导你完成超额付款

同源策略的核心意义,就是隔离不同站点的资源,防止恶意网站窃取用户的敏感数据,从根源上阻断绝大多数 CSRF、XSS 衍生的攻击行为

二、主流跨域解决方案

2.1 CORS:跨域资源共享的核心机制

2.1.1 CORS 工作原理

CORS(Cross-Origin Resource Sharing)是 W3C 标准,通过HTTP 响应头告知浏览器是否允许跨域访问。其核心流程分为两类:

简单请求(Simple Request)

满足以下条件的请求为简单请求:

  • 方法仅限 GETHEADPOST
  • Content-Typeapplication/x-www-form-urlencodedmultipart/form-datatext/plain
  • 无自定义请求头

简单请求直接发送,无需预检。

非简单请求(Preflight Request)

复杂请求会触发浏览器的预检机制

  1. 浏览器先使用 OPTIONS 方法发起一个「预检请求」,携带三个核心请求头:

    1. Origin:请求的来源域
    2. Access-Control-Request-Method:正式请求要使用的方法
    3. Access-Control-Request-Headers:正式请求要携带的自定义头
    // 预检请求示例
    OPTIONS /api/data HTTP/1.1
    Origin: https://www.example.com
    Access-Control-Request-Method: PUT
    Access-Control-Request-Headers: Authorization, Content-Type
  2. 服务器接收预检请求,校验后返回对应的 CORS 响应头,明确是否允许该请求

  3. 浏览器校验预检响应通过后,才会发起正式的 HTTP 请求;校验失败则直接拦截,正式请求永远不会发出

2.1.2 核心 CORS 响应头

服务器的 CORS 配置,本质就是正确设置以下 HTTP 响应头,每一个都有明确的作用,不可乱配:

响应头必需性核心作用安全注意事项
Access-Control-Allow-Origin必需指定允许访问的源,值可以是单个具体源、null,或通配符*非公开 API 严禁使用*;携带凭据时,必须为具体源,绝对不能用*
Access-Control-Allow-Methods预检必需指定正式请求允许的 HTTP 方法,多个用逗号分隔按需开放,不要全量开放GET,POST,PUT,DELETE,PATCH
Access-Control-Allow-Headers预检必需指定正式请求允许携带的自定义请求头按需开放,比如仅开放Content-Type,Authorization
Access-Control-Allow-Credentials可选指定是否允许请求携带 Cookie、HTTP 认证等身份凭证,值只能是true开启后,Allow-Origin不能为*,前端必须设置withCredentials: true
Access-Control-Max-Age可选指定预检请求的缓存时间(秒),缓存期内不会重复发起预检建议设置合理值(如 86400 秒 = 1 天),减少 OPTIONS 请求开销
// 使用示例
Access-Control-Allow-Origin: https://www.example.com  # 必须,允许的源
Access-Control-Allow-Methods: GET, POST, PUT, DELETE   # 允许的HTTP方法
Access-Control-Allow-Headers: Content-Type, Authorization  # 允许的请求头
Access-Control-Allow-Credentials: true  # 是否允许发送Cookie
Access-Control-Max-Age: 86400  # 预检请求缓存时间(秒)
Access-Control-Expose-Headers: X-Total-Count  # 允许客户端访问的响应头

2.2 服务端 CORS 配置(生产环境首选)

Node.js/Express 方案

const express = require('express');
const cors = require('cors');
const app = express();

// 方案1:使用cors中间件(推荐)
app.use(cors({
  origin: 'https://www.example.com',  // 精确指定源
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  credentials: true,  // 允许携带Cookie
  maxAge: 86400,      // 预检缓存24小时
}));

// 方案2:手动配置(更灵活)
app.use((req, res, next) => {
  const allowedOrigins = ['https://www.example.com', 'https://app.example.com'];
  const origin = req.headers.origin;
  
  if (allowedOrigins.includes(origin)) {
    res.setHeader('Access-Control-Allow-Origin', origin);
    res.setHeader('Access-Control-Allow-Credentials', 'true');
  }
  
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With');
  res.setHeader('Access-Control-Max-Age', '86400');
  
  if (req.method === 'OPTIONS') {
    return res.sendStatus(200);
  }
  
  next();
});

Spring Boot 4.0 方案

@Configuration
public class CorsConfig implements WebMvcConfigurer {
    
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
                .allowedOrigins("https://www.example.com", "https://app.example.com")
                .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
                .allowedHeaders("Content-Type", "Authorization", "X-Requested-With")
                .allowCredentials(true)
                .maxAge(86400); // 24小时
    }
    
    // 或使用@CrossOrigin注解
    @RestController
    @RequestMapping("/api")
    @CrossOrigin(origins = "https://www.example.com", maxAge = 86400)
    public class ApiController {
        // ...
    }
}

FastAPI 方案

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI()

# 配置CORS中间件
app.add_middleware(
    CORSMiddleware,
    allow_origins=["https://www.example.com", "https://app.example.com"],
    allow_credentials=True,
    allow_methods=["*"],  # 或指定具体方法
    allow_headers=["*"],  # 或指定具体头
    max_age=86400,  # 预检缓存24小时
)

2.2 代理方案

代理方案的核心原理,直击同源策略的本质:同源策略仅限制浏览器与服务器之间的通信,服务器与服务器之间的 HTTP 通信,没有任何跨域限制

我们只需要搭建一个「代理中转服务器」,让浏览器把请求发给同源的代理服务器,再由代理服务器转发给真实的后端接口,就能彻底绕开跨域问题 —— 对浏览器来说,请求是发给同源的代理服务,不存在跨域;对后端来说,请求来自正常的服务器,无需做任何 CORS 配置。其中,代理方案分为开发环境和生产环境两类。

开发环境代理

Vite/Vue3 配置

// vite.config.js
export default defineConfig({
  server: {
    proxy: {
      '/api': {
        target: 'https://api.example.com',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, ''),
        secure: false,  // 不验证SSL证书
        ws: true,       // 代理WebSocket
      }
    }
  }
})

Webpack DevServer 配置

// webpack.config.js
module.exports = {
  devServer: {
    proxy: {
      '/api': {
        target: 'https://api.example.com',
        pathRewrite: { '^/api': '' },
        changeOrigin: true,
        headers: {
          Connection: 'keep-alive'
        }
      }
    }
  }
}

配置完成后,前端只需把请求地址写成/api/xxx,开发服务器会自动把请求转发到http://localhost:3000/xxx,浏览器全程认为是同源请求,不会有任何跨域问题。

Nginx 反向代理(生产环境推荐)

生产环境中,我们通常使用 Nginx 作为静态资源服务器和反向代理服务器,实现和开发代理完全一致的效果,同时还能兼顾负载均衡、静态资源缓存等能力。

server {
    listen 80;
    server_name www.example.com;
    
    # 前端静态资源
    location / {
        root /var/www/frontend;
        index index.html;
        try_files $uri $uri/ /index.html;
    }
    
    # API代理,解决跨域
    location /api/ {
        proxy_pass https://api.example.com/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        
        # CORS相关头
        add_header 'Access-Control-Allow-Origin' 'https://www.example.com' always;
        add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
        add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization' always;
        add_header 'Access-Control-Allow-Credentials' 'true' always;
        
        # 处理预检请求
        if ($request_method = 'OPTIONS') {
            add_header 'Access-Control-Max-Age' 86400;
            add_header 'Content-Type' 'text/plain; charset=utf-8';
            add_header 'Content-Length' 0;
            return 204;
        }
    }
}

配置完成后,用户访问 https://www.example.com 加载前端页面,所有 /api 开头的请求都会被 Nginx 转发到后端服务,前端和接口处于完全同源的状态,从根源上彻底消除了跨域问题,无需后端做任何 CORS 配置。

2.3 JSONP:仅兼容老旧浏览器的古董方案

JSONP 是早期前端解决跨域的方案,核心原理是利用<script>标签不受同源策略限制的特性,通过动态创建 script 标签,请求后端返回一个「函数调用」,把数据作为参数传入,前端在全局定义该函数,从而拿到跨域数据。

2.4 postMessage:跨窗口 /iframe 通信专属方案

postMessage 是 HTML5 提供的 API,专门用于解决「不同源的页面之间」的通信问题,最常见的场景:主页面与嵌入的跨域 iframe 通信、同一浏览器打开的多个跨域标签页通信。

2.5 WebSocket:全双工通信无跨域限制

WebSocket 协议本身不受浏览器同源策略限制,只要服务器支持,前端可以和任意源的 WebSocket 服务建立连接,实现全双工通信,是实时通信场景的最优解。

2.3 跨域解决方案对比与选型建议

方案适用场景优点缺点安全性
服务端 CORS生产环境标准化、浏览器原生支持需要后端配合★★★★☆
Nginx 代理生产环境性能好、配置灵活运维复杂度高★★★★★
开发代理开发环境无需后端改动、调试方便仅限开发环境★★★★☆
JSONP传统项目兼容性好仅支持 GET、有安全风险★☆☆☆☆
WebSocket实时通信无跨域限制仅适用于特定场景★★★★☆

选型建议

  • 生产环境:优先选择 Nginx 反向代理或服务端 CORS 配置
  • 开发环境:使用 Webpack/Vite 内置代理
  • 遗留系统:考虑 WebSocket 或服务端代理
  • 移动端/小程序:服务端统一代理,避免前端跨域问题

If you enjoyed this, leave a comment~