JavaScript中的跨域问题

一、跨域的本质:同源策略为何存在?

1. 什么是同源策略?

浏览器的核心安全机制,要求协议、域名、端口三者完全一致才视为「同源」,否则即为跨域。例如:

2. 同源策略保护什么?
  • 防止 DOM 劫持(如 iframe 跨域操作)
  • 防止 Cookie/Storage 被非法读取
  • 防止 XMLHttpRequest/Fetch 跨域请求数据泄露

二、常见跨域场景(开发中必遇)

  1. 前后端分离:前端(3000 端口)→ 后端 API(8080 端口)
  1. 第三方接口调用:前端 → 高德地图 / 微信支付 API
  1. 跨域资源加载:CDN 引入脚本、图片、字体(部分场景触发跨域)
  1. 跨窗口通信:父页面(a.com)→ 子 iframe(b.com

三、7 种主流跨域解决方案(含实战代码)

方案 1:CORS(跨域资源共享)- 推荐首选

原理:后端通过响应头告知浏览器「允许该域名跨域访问」,支持 GET/POST/PUT/DELETE 等所有请求方法,兼容所有现代浏览器。

实战配置(分语言)
  • Node.js(Express)
复制代码

// 安装cors中间件:npm i cors

const express = require('express');

const cors = require('cors');

const app = express();

// 全局允许跨域(开发环境)

app.use(cors({

origin: 'http://localhost:3000', // 允许的前端域名(生产环境指定具体域名,不要用*)

credentials: true, // 允许携带Cookie

methods: ['GET', 'POST', 'PUT', 'DELETE'], // 允许的请求方法

allowedHeaders: ['Content-Type', 'Authorization'] // 允许的请求头

}));

app.get('/api/data', (req, res) => {

res.json({ message: '跨域请求成功' });

});

app.listen(8080);

  • Java(Spring Boot)
复制代码

// 全局配置类

@Configuration

public class CorsConfig implements WebMvcConfigurer {

@Override

public void addCorsMappings(CorsRegistry registry) {

registry.addMapping("/**") // 所有接口

.allowedOrigins("http://localhost:3000") // 允许的前端域名

.allowedMethods("*") // 允许所有方法

.allowedHeaders("*") // 允许所有请求头

.allowCredentials(true) // 允许携带Cookie

.maxAge(3600); // 预检请求缓存时间(1小时)

}

}

  • 前端请求(Axios)
复制代码

axios({

url: 'http://localhost:8080/api/data',

method: 'GET',

withCredentials: true // 关键:允许携带Cookie(需与后端credentials:true配套)

}).then(res => console.log(res.data));

核心细节:
  • 简单请求(GET/POST,请求头无自定义字段):直接发送请求,后端返回 Access-Control-Allow-Origin 即可。
  • 预检请求(PUT/DELETE、自定义头、Content-Type: application/json):浏览器先发送 OPTIONS 请求校验,通过后再发送真实请求。
方案 2:JSONP - 兼容老浏览器(仅支持 GET)

原理:利用<script>标签不受同源策略限制的特性,通过回调函数获取跨域数据。

实战代码:
  • 后端(Node.js):
复制代码

app.get('/api/jsonp', (req, res) => {

const callback = req.query.callback; // 前端传入的回调函数名

const data = JSON.stringify({ message: 'JSONP跨域成功' });

res.send(`${callback}(${data})`); // 执行回调函数

});

  • 前端:
复制代码

function handleJsonp(data) {

console.log(data); // 接收跨域数据

}

// 动态创建script标签

const script = document.createElement('script');

script.src = 'http://localhost:8080/api/jsonp?callback=handleJsonp';

document.body.appendChild(script);

优缺点:
  • 优点:兼容 IE6+,实现简单。
  • 缺点:仅支持 GET 请求,存在 XSS 安全风险,无法捕获错误。
方案 3:代理服务器 - 开发 / 生产环境通用

原理:通过同源的代理服务器转发跨域请求(浏览器→代理→目标服务器),因为代理与前端同源,不存在跨域问题。

场景 1:开发环境(Webpack/Vite)
  • Webpack 配置(vue.config.js)
复制代码

module.exports = {

devServer: {

proxy: {

'/api': { // 匹配所有以/api开头的请求

target: 'http://localhost:8080', // 目标后端地址

changeOrigin: true, // 允许跨域

pathRewrite: { '^/api': '' } // 去除请求路径中的/api前缀

}

}

}

};

  • 前端请求:
复制代码

// 此时请求的是代理服务器(同源),由代理转发到8080端口

axios.get('/api/data').then(res => console.log(res.data));

场景 2:生产环境(Nginx 代理)
复制代码

server {

listen 80;

server_name localhost;

# 代理前端页面

location / {

root /usr/share/nginx/html;

index index.html;

}

# 代理后端API

location /api/ {

proxy_pass http://localhost:8080/; # 转发到后端地址

proxy_set_header Host $host;

proxy_set_header X-Real-IP $remote_addr;

}

}

方案 4:document.domain - 主域名相同场景

原理 :当两个页面主域名相同(如a.test.comb.test.com),可通过设置document.domain = 'test.com'实现跨域通信。

实战代码:
复制代码

<iframe src="http://b.test.com/child.html" id="iframe"></iframe>

<script>

document.domain = 'test.com'; // 设置主域名

window.onload = function() {

const iframe = document.getElementById('iframe');

console.log(iframe.contentWindow.data); // 访问子页面数据

};

</script>

复制代码

document.domain = 'test.com'; // 与父页面保持一致

window.data = '子页面跨域数据';

方案 5:postMessage - 跨窗口 /iframe 通信

原理:HTML5 新增 API,允许不同域名的窗口 /iframe 之间安全传递数据。

实战代码:
  • 发送方(a.com):
复制代码

const iframe = document.getElementById('iframe');

// 触发时机:确保iframe加载完成

iframe.onload = function() {

// 目标窗口、数据、允许的域名

iframe.contentWindow.postMessage('Hello 跨域窗口', 'http://b.com');

};

  • 接收方(b.com):
复制代码

window.addEventListener('message', (e) => {

// 验证发送方域名(关键:防止恶意攻击)

if (e.origin === 'http://a.com') {

console.log(e.data); // 接收数据:Hello 跨域窗口

// 回复数据

e.source.postMessage('收到消息', e.origin);

}

});

方案 6:WebSocket - 全双工跨域通信

原理:WebSocket 协议不受同源策略限制,建立连接后可双向实时通信(适用于聊天、实时数据推送)。

实战代码:
  • 后端(Node.js + ws):
复制代码

const WebSocket = require('ws');

const wss = new WebSocket.Server({ port: 8080 });

wss.on('connection', (ws) => {

ws.on('message', (message) => {

console.log('收到客户端消息:', message);

ws.send('WebSocket跨域通信成功'); // 发送消息给客户端

});

});

  • 前端:
复制代码

const ws = new WebSocket('ws://localhost:8080');

ws.onopen = () => {

ws.send('前端请求连接');

};

ws.onmessage = (e) => {

console.log('收到服务端消息:', e.data);

};

方案 7:Access-Control-Allow-Origin: * - 简单场景应急

适用场景:公开 API(无需携带 Cookie、无自定义头),直接在后端设置响应头:

复制代码

// Node.js示例

app.get('/api/public', (req, res) => {

res.setHeader('Access-Control-Allow-Origin', '*'); // 允许所有域名跨域

res.json({ message: '公开API跨域成功' });

});

⚠️ 注意:设置*时,后端不能开启credentials: true(Cookie 传递),否则浏览器会报错。

四、解决方案选型指南(90% 场景覆盖)

|------------------|---------------------------------|-------------------------|
| 场景 | 推荐方案 | 不推荐方案 |
| 前后端分离(开发 / 生产) | CORS + 代理服务器 | JSONP |
| 兼容 IE6/7 老浏览器 | JSONP | CORS |
| 跨窗口 /iframe 通信 | postMessage | document.domain(仅主域名相同) |
| 实时通信(聊天 / 推送) | WebSocket | 轮询(效率低) |
| 公开 API(无 Cookie) | Access-Control-Allow-Origin: * | - |
| 主域名相同(子域名不同) | document.domain | CORS(略显冗余) |

五、避坑指南(90 分必备细节)

  1. *CORS 携带 Cookie 时,origin 不能设为 **:必须指定具体域名(如http://localhost:3000),且前后端需同时开启credentials: true。
  1. 预检请求失败:检查后端是否允许OPTIONS方法,是否配置了Access-Control-Allow-Headers(对应前端自定义头)。
  1. JSONP XSS 风险:后端需过滤回调函数名(如禁止特殊字符),前端避免使用不可信的第三方 JSONP 接口。
  1. 代理服务器路径匹配:Webpack 代理的pathRewrite需注意是否去除前缀,避免后端接口 404。
  1. postMessage 安全风险:接收方必须验证e.origin,防止恶意网站伪造消息。
  1. CDN 资源跨域:图片 / 字体跨域时,CDN 需设置Access-Control-Allow-Origin,前端<img>标签无需额外配置(浏览器自动处理)。

六、总结

跨域问题的核心是浏览器同源策略,解决方案的本质都是「绕开或合规突破该策略」。实际开发中,CORS + 代理服务器 是最通用、最安全的组合(覆盖 90% 场景),其他方案根据特殊需求选型(如老浏览器用 JSONP、实时通信用 WebSocket)。

记住:跨域的核心是「后端授权」,前端方案仅为辅助,最终必须通过后端配置实现合法跨域(前端绕过同源策略的方案均存在安全风险,不推荐生产环境使用)。

相关推荐
San30.13 分钟前
深入 JavaScript 内存机制:从栈与堆到闭包的底层原理
开发语言·javascript·udp
Fantastic_sj29 分钟前
Vue3相比Vue2的改进之处
前端·javascript·vue.js
灰灰勇闯IT33 分钟前
RN路由与状态管理:打造多页面应用
开发语言·学习·rn路由状态
wd_cloud33 分钟前
QT/6.7.2/Creator编译Windows64 MySQL驱动
开发语言·qt·mysql
亭上秋和景清37 分钟前
指针进阶:函数指针详解
开发语言·c++·算法
胡萝卜3.038 分钟前
C++现代模板编程核心技术精解:从类型分类、引用折叠、完美转发的内在原理,到可变模板参数的基本语法、包扩展机制及emplace接口的底层实现
开发语言·c++·人工智能·机器学习·完美转发·引用折叠·可变模板参数
9ilk39 分钟前
【C++】--- C++11
开发语言·c++·笔记·后端
biter down1 小时前
C++ 函数重载:从概念到编译原理
开发语言·c++
ttod_qzstudio2 小时前
深入理解 TypeScript 数组的 find 与 filter 方法:精准查找的艺术
javascript·typescript·filter·find
冬男zdn2 小时前
优雅处理数组的几个实用方法
前端·javascript