跨域解决方案梳理

从浏览器的同源策略说起

浏览器为确保资源安全,而遵循的一种策略,它限制从一个源加载的网页如何与另一个源的资源进行交互,以防止恶意网站窃取用户的敏感数据。

若没有同源策略,浏览器很容易受到XSS、CSRF等攻击。

其核心规则是: "同源"需满足以下三个条件

  1. 协议相同 (如 httphttps 不同源);
  2. 域名相同 (如 example.comapi.example.com 不同源);
  3. 端口相同 (如 example.com:80example.com:8080 不同源)。

受限制的操作


什么是跨域?

当请求的目标与当前页面不满足同源条件时,即发生跨域。

常见跨域场景

当前页面URL 目标URL 是否跨域 原因
http://example.com/a http://example.com/b 同协议、域名、端口
https://example.com http://example.com 协议不同(https vs http
http://example.com http://api.example.com 域名不同(子域名差异)
http://example.com:80 http://example.com:8080 端口不同

子域名不同是否算跨域?

是的,子域名不同属于跨域。例如:

  • www.example.comapi.example.com 是不同源(主域名相同但子域名不同);
  • blog.example.comshop.example.com 也是跨域。

特别说明:

1.跨域限制仅存在浏览器端,服务端不存在跨域限制。 2.即使跨域,Ajax请求也可以正常发出,但响应数据不会交给开发者。


CORS

CORS概述

CORS 全称:Cross-Origin Resource Sharing (跨域资源共享),是用于控制浏览器校验跨域请求 的一套规范,服务器依照CORS规范,添加特定响应头来控制浏览器校验,大致规则如下:

  • 服务器明确表示拒绝跨域 请求,或没有表示 ,则浏览器校验不通过
  • 服务器明确表示允许跨域 请求,则浏览器校验通过

备注说明:使用CORS解决跨域是最正统的方式,且要求服务器是"自己人"

常见的响应头设置:

  1. Access-Control-Allow-Origin : 这个头部指定允许访问资源的域。它可以设置为特定的域名,如https://example.com,也可以使用通配符*表示允许任何域访问。

  2. Access-Control-Allow-Methods: 该头部用于预检请求中,告知客户端允许的实际请求方法(如GET、POST、PUT等)。这在服务器响应OPTIONS请求时非常有用。

  3. Access-Control-Allow-Headers: 这个头部指定了除了简单的默认头部之外,哪些HTTP头部可以用于实际的请求。这对于非简单请求特别重要,因为它们可能包含自定义头部。

  4. Access-Control-Max-Age: 预检请求的结果可以被缓存多长时间,以秒为单位。这样可以避免频繁发送OPTIONS预检请求。

  5. Access-Control-Allow-Credentials : 表明是否允许发送凭据(包括Cookies和HTTP认证信息)作为请求的一部分。注意,如果设置此值为true,则Access-Control-Allow-Origin不能使用通配符*

简单请求和复杂请求

CORS会把请求分为两类,分别是:

1.简单请求

  • 方法(Method) :请求方法必须是 GETPOSTHEAD

  • 头部(Headers) :请求头必须是浏览器默认的几种头部,不能包含自定义头部。具体来说,只允许以下这些头部(除非 CORS 服务器允许):

    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type(仅限 text/plainmultipart/form-dataapplication/x-www-form-urlencoded

简记:只要不手动修改请求头,一般都能符合该规范。

2.复杂请求

  • 不是简单请求,就是复杂请求。
  • 复杂请求会自动发送预检请求

CORS解决简单请求

对于简单请求Access-Control-Allow-Origin 是必须的,这是跨域请求能否成功的关键。如果请求不涉及凭证,Access-Control-Allow-Origin 可以设置为 *,允许所有源进行跨域请求;如果请求涉及凭证,则必须指定明确的源。

如果只是在使用标准的跨域请求方法(GET, POST, HEAD),并且没有使用自定义的请求头部和特殊的 Content-Type,其他响应头部(如 Access-Control-Allow-MethodsAccess-Control-Allow-Headers 等)通常是可选的,浏览器会自动允许这些请求。

大致思路:

如果要发送cookie的话必须满足:

1.web 请求设置withCredentials

2.Access-Control-Allow-Credentialstrue

3.Access-Control-Allow-Origin为非 *

后面两个都是服务端配置

示例:

js 复制代码
var xhr = new XMLHttpRequest();
//简单跨域请求
xhr.open('POST', 'http://localhost:8088/data1', true);
//携带 cookie
xhr.withCredentials = true;
// 对于简单请求,如何设置了不符合简单请求的请求头,就会变成复杂请求
// xhr.setRequestHeader('abc', '123');
xhr.send(JSON.stringify({ key: 'value' }))

xhr.onreadystatechange = function() {
  if (xhr.readyState === 4) {
    if (xhr.status >= 200 && xhr.status < 300) {
      console.log(xhr.responseText);
      console.log(xhr.status, 'CORS Preflight successful');
    } else {
      console.error('CORS Preflight failed');
    }
  }
}
js 复制代码
const http = require('http');

// 调用createServer创建http服务
const server = http.createServer((req, res) => {
    const headers = {
        'Access-Control-Allow-Origin': req.headers.origin || '*', 
        'Access-Control-Allow-Credentials': 'true',
    }
    if(req.method === 'GET' && req.url === '/data0') {
        res.writeHead(200, headers);
        res.end('GET 请求成功');
        return; // 终止请求处理
    }
    if(req.method === 'POST' && req.url === '/data1') {
        let body = []
        req
          .on('data',(chunk)=>{
            body.push(chunk) 
          })
          .on('end',()=>{
            console.log(body.toString())
            res.writeHead(201, headers);
            res.end('POST 请求成功');
          })

        return; // 终止请求处理 
    }
    res.writeHead(404, headers);
    res.end('404 Not Found');
});
server.listen(8088, () => {
  console.log('服务器启动成功!')
})

CORS解决复杂请求

对于复杂请求 ,浏览器在实际发出请求之前,会先发出 预检请求(preflight request)。服务器必须对预检请求做出正确响应,才能允许实际的跨域请求。

预检请求是一个 OPTIONS 请求,浏览器会询问服务器是否允许特定的跨域请求。预检请求会携带以下信息:

  • Access-Control-Request-Method :实际请求将使用的方法(例如,PUTDELETE)。
  • Access-Control-Request-Headers:实际请求中包含的自定义请求头。
  • Origin:发起请求的源。

服务器必须响应以下 CORS 响应头

  • Access-Control-Allow-Origin :指定允许跨域的源。如果服务器允许所有源,返回 *,或者可以指定具体的源(如 https://example.com)。

  • Access-Control-Allow-Methods :列出允许的 HTTP 方法。例如,PUTDELETEPATCH 等方法必须在此列出。

  • Access-Control-Allow-Headers:列出允许的自定义请求头。预检请求会告知服务器实际请求将携带哪些头部,服务器必须在响应中允许这些头部。

可选:

  • Access-Control-Max-Age:设置预检请求的结果可以缓存多长时间(以秒为单位)。通常设置为几小时,以减少频繁发送预检请求。

关于cookie的发送,与前面所诉一样。

示例:

js 复制代码
var xhr = new XMLHttpRequest();
// 复杂跨域请求
xhr.open('PUT', 'http://localhost:3000/data', true);
xhr.setRequestHeader('Content-Type', 'application/json');
//自定义请求头,需要在Access-Control-Allow-Headers中显示声明
xhr.setRequestHeader('abc', '123');
xhr.withCredentials = true;
xhr.send(JSON.stringify({ key: 'value' }))

xhr.onreadystatechange = function() {
  if (xhr.readyState === 4) {
    if (xhr.status >= 200 && xhr.status < 300) {
      console.log(xhr.responseText,'11');
      console.log(xhr.status, 'CORS Preflight successful');
    } else {
      console.error('CORS Preflight failed');
    }
  }
}
js 复制代码
const http = require('http');

const server = http.createServer((req, res) => {
  const headers = {
    'Access-Control-Allow-Credentials': 'true',
    'Access-Control-Allow-Origin': req.headers.origin || '*',
    'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
    'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Custom-Header,abc',
    'Access-Control-Max-Age': '86400',
  };

  if (req.method === 'OPTIONS') {
    // 预检请求 200 201 Created 204 No Content
    res.writeHead(204, headers);
    console.log('预检请求');
    res.end();
    return; // 终止请求处理,避免继续执行后续代码
  }

  if (req.method === 'PUT' && req.url === '/data') {
    res.writeHead(201, headers);
    res.end('PUT 请求成功');
    return; // 终止请求处理
  }
  res.writeHead(404, headers);
  res.end('404 Not Found');
});

server.listen(3000, () => {
  console.log('服务器正在监听端口 3000');
});

JSONP

JSONP 概述

JSONP 是利用了<script>标签可以跨域加载脚本,且不受严格限制的特性,可以说是程序员智慧的结晶,早期一些浏览器不支持 CORS 的时,可以靠 JSONP 解决跨域。

基本流程

  • 第一步: 客户端创建一个<script>标签,并将其src属性设置为包含跨域请求的 URL,同时准备一个回调函数,这个回调函数用于处理返回的数据。

  • 第二步: 服务端接收到请求后,将数据封装在回调函数中并返回。

  • 第三步: 客户端的回调函数被调用,数据以参数的形式传入回调函数。

缺点

  1. 仅支持GET请求:JSONP只能通过GET方法发送请求,无法使用POST、PUT、DELETE等其他HTTP方法。
  2. 错误处理困难:如果请求失败,很难获取详细的错误信息,浏览器通常只会显示脚本加载失败的通用错误。
  3. 安全问题: JSONP从外部域加载并执行JavaScript代码,容易遭到XSS攻击。
  4. 性能问题 :每个JSONP请求都需要动态创建<script>标签,可能影响页面性能。

实现案例可以参考我之前写的文章

postMessage

概述

postMessageHTML5 的跨文档消息传递 API ,用于在不同窗口、iframe 或不同源的网页之间安全地传递消息 ,常用于跨域通信

iframe 跨域通信

问题 :如果一个网站在 iframe 中嵌套了一个 不同源 的网页,默认情况下,它们不能直接访问彼此的 JavaScript 对象(受同源策略限制)。

解决方案 :使用 postMessage 让主页面和 iframe 之间安全通信。

示例:主页面向 iframe 发送消息

主页面(https://parent.com):

xml 复制代码
<iframe id="myFrame" src="https://child.com"></iframe>

<script>
    const iframe = document.getElementById("myFrame");
    
    // 向 iframe 发送消息
    iframe.onload = function() {
        iframe.contentWindow.postMessage("Hello from parent", "https://child.com");
    };

    // 监听 iframe 返回的消息
    window.addEventListener("message", (event) => {
        if (event.origin === "https://child.com") {
            console.log("Received from child:", event.data);
        }
    });
</script>

iframe(https://child.com)监听消息并回复

xml 复制代码
<script>
    window.addEventListener("message", (event) => {
        if (event.origin !== "https://parent.com") return; // 只接受特定来源的消息
        console.log("Received from parent:", event.data);
        
        // 回复消息给主页面
        event.source.postMessage("Hello from child", event.origin);
    });
</script>

websocket

WebSocket 也是用于通信的,但它与 HTTP 不同:

  1. 通信方式不同

    • XMLHttpRequestFetch API 是基于 HTTP 的请求-响应模式,请求数据后服务器返回结果,连接随即关闭。
    • WebSocket 是全双工通信协议 ,可以保持连接,客户端和服务器可以 随时相互推送数据,不像 HTTP 需要客户端主动发起请求。
  2. 不受同源策略限制的原因

    • WebSocket 使用 ws://wss:// 协议(安全 WebSocket)。
    • 在建立 WebSocket 连接时,客户端会先通过 HTTP 发送一次 Upgrade 请求 ,要求服务器将连接升级为 WebSocket 连接(即 Connection: Upgrade)。
    • 一旦连接成功,WebSocket 就不会再受同源策略的约束,因为它不再是普通的 HTTP 请求,而是一个持久化的双向通道。

案例:

服务端:

js 复制代码
const WebSocket = require('ws');
const http = require('http');

// 1. 创建一个 HTTP 服务器
// 2. 创建一个 WebSocket 服务器
// 3. 建立一次http连接

const server = http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'text/plain' });
  res.end('WebSocket server is running');
})


const wss = new WebSocket.Server({ 
    server,
    path: '/ws', // 与前端保持一致
 });

wss.on('connection', (ws) => {
  console.log('client connected');
  ws.on('message', (message) => {
    console.log('received: %s', message);
    // 向客户端发送消息
    ws.send('Hello from server!');
  });
   ws.send('Hello from server!');
})


server.listen(8080, () => {
  console.log('webSocket server is running on port 8080');
})

客户端:

js 复制代码
// websocket 不受同源策略的影响
const ws = new WebSocket('ws://localhost:8080/ws');
ws.onopen = function () {
    console.log('连接成功');
    ws.send('hello from client');
}
ws.onmessage = function (e) {
    console.log('message from server',e.data); 
}

vite反向代理

反向代理是一种 服务器代理机制,即前端开发服务器通过代理将请求转发到目标服务器。这样,浏览器只会和开发服务器通信,而不直接请求跨域的后端服务器,从而避免跨域问题。

Vite 中配置反向代理

在 Vite 项目中,可以通过 vite.config.js 文件来配置代理。

js 复制代码
import { defineConfig } from 'vite'

export default defineConfig({
  server: {
    proxy: {
      // 代理 /api 路径到后端服务
      '/api': {
        target: 'http://localhost:5000',  // 后端服务的地址
        changeOrigin: true,              // 如果是跨域请求,设置为 true
        rewrite: (path) => path.replace(/^\/api/, ''),  // 可选:重写路径,去掉 /api 前缀
      },
    },
  },
})

总结

JSONP 通过动态 <script> 标签绕过同源限制,支持 GET 请求,安全性差。CORS 通过设置 HTTP 头部,允许跨域请求,现代浏览器广泛支持,是通信双方协商后的结果,算是真正的跨域请求解决方案。postMessage 用于不同源窗口之间安全通信,避免跨域问题。WebSocket 使用 HTTP 协议握手,连接后不受同源策略限制,适用于实时通信。Vite 反向代理 通过代理请求到目标服务器,解决开发时的跨域问题,简化配置。

相关推荐
浪遏1 小时前
我的远程实习(六) | 一个demo讲清Auth.js国外平台登录鉴权👈|nextjs
前端·面试·next.js
拉不动的猪3 小时前
vue与react的简单问答
前端·javascript·面试
牛马baby3 小时前
Java高频面试之并发编程-02
java·开发语言·面试
yuanbenshidiaos4 小时前
面试问题总结:qt工程师/c++工程师
c++·qt·面试
uhakadotcom4 小时前
Langflow:打造AI应用的强大工具
前端·面试·github
uhakadotcom4 小时前
🤖 LangGraph 多智能体群集
面试·架构·github
uhakadotcom5 小时前
Caddy Web服务器初体验:简洁高效的现代选择
前端·面试·github
uhakadotcom5 小时前
NVIDIA Resiliency Extension(NVRx)简介:提高PyTorch训练的容错性
算法·面试·github
专业抄代码选手5 小时前
【JS】instanceof 和 typeof 的使用
前端·javascript·面试
雷渊5 小时前
深入分析mybatis中#{}和${}的区别
java·后端·面试