跨域问题:从同源策略到JSONP、CORS实战,前端必知必会

跨域问题:从同源策略到JSONP实战,前端必知必会

前端面试中几乎必问的跨域问题,你真的理解透彻了吗?今天我们就来彻底搞懂它!

什么是跨域?浏览器为何如此"小气"

想象一下,你正在本地开发一个React应用(运行在5173端口),需要调用后端API(运行在8080端口)。当你信心满满地发起请求时,浏览器却无情地抛出了错误:

csharp 复制代码
Access to fetch at 'http://localhost:8080/api/data' from origin 'http://localhost:5173' 
has been blocked by CORS policy

这就是经典的跨域问题!浏览器为何如此"小气"?这要从同源策略说起。

同源策略:浏览器的安全卫士

同源策略(Same-Origin Policy) 是浏览器最基本的安全机制。它规定:只有当两个URL的协议(protocol)、域名(domain)和端口(port) 完全相同时,才被认为是同源。

让我们看几个例子:

当前页面URL 请求URL 是否同源 原因
http://a.com/index http://a.com/api ✅ 是 协议、域名、端口都相同
http://a.com:8080 http://a.com:3000/api ❌ 否 端口不同
https://a.com http://a.com/api ❌ 否 协议不同
http://a.com http://b.com/api ❌ 否 域名不同

为何要有同源策略?

没有同源策略的世界会多么可怕!想象一下:

  1. 你登录了银行网站(bank.com
  2. 然后访问了一个恶意网站(evil.com
  3. 恶意网站可以在后台悄悄请求bank.com/transfer?to=attacker&amount=10000
  4. 因为浏览器会自动携带bank.com的cookie,转账可能成功!

同源策略就像浏览器的"边防检查",保护我们免受:

  • CSRF攻击:跨站请求伪造
  • 数据窃取:防止恶意网站读取用户敏感数据
  • DOM泄露:阻止恶意网站操作其他网站的DOM

跨域解决方案:突破"边防"的多种方式

既然同源策略如此严格,我们如何在开发中解决跨域问题呢?下面是几种常用方法:

1. JSONP:巧用Script标签的"漏洞"

JSONP 是一种绕过同源策略限制的"古老"但有效的方法 ,它利用 <script> 标签不受同源策略限制的特点,通过动态创建 <script> 标签来加载跨域数据。
⚠️ 注意:JSONP 只支持 GET 请求,不支持 POST 或其他方法。

实现原理
  1. 前端创建一个<script>标签,src指向跨域API并携带回调函数名

    html 复制代码
    <script src="http://api.com/data?callback=handleData"></script>
  2. 后端返回的不是JSON,而是包裹在回调函数中的JSON数据

    js 复制代码
    const data = { code:0, msg:'字节,我来了' } 
    res.end("callback("+JSON.stringify(data)+")")

    因为我们毕竟是包裹在script标签内,要遵守script标签内容的规则,所以要用回调函数包裹,将返回的数据变成js格式

    js 复制代码
    callback({
      "name": "小明",
      "age": 25
    });

3. 前端预先定义好回调函数

js 复制代码
function callback(data) {
  console.log(data); // 获取到跨域数据!
}
手写JSONP实现

一直像上面一样既要保证前端使用的是script标签去请求数据,还要让前后端共同使用一个在前端声明的使JSON文件变成JS的回调函数,还要在前后端不同的地方反复调用,使用方法非常复杂,于是我们便想将JSONP封装成一个函数,之后每次请求时,只要调用这个函数即可,调用这个函数就像使用可以跨域的fetch一样方便

前端封装:

JSONP函数包含的:

  • 将跨域请求包装成一个script文件,并挂载到DOM上执行
  • 定义callback全局函数,在这个函数可以接受到后端返回的数据
  • 将回调函数,基础的URL和其他各种参数传入JSONP函数中,在函数里面将这些传入的参数拼凑成一个script的src地址(注意queryString对象之前要加上?
  • 为了确保一定会传递callback函数,我们在JSONP函数里面再将callback函数和其他查询参数放到一起
html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <script>
        function getJSONP ({url,params={},callback}){
            //`callback${Date.now()}`避免重复,覆盖别人的
            //DOM
            return new Promise((resolve,reject)=>{
                let script = document.createElement('script')
                params = {...params,callback}
                // queryString的对象 ?callback=show&a=1&b-2
                let arr = []
                for(let key in params){
                    arr.push(`${key}=${params[key]}`)
                }
                console.log(arr)
                // queryString 以?开始 查询字符串
                script.src = `${url}?${arr.join('&')}`
                window[callback] = (data) =>{
                    resolve(data)
                }//在页面上声明了一个全局的函数

                document.body.appendChild(script)
            })
        }
        getJSONP({
            url:'http://localhost:3000/say',
            params:{//查询参数
                wd:'iloveyou'
            },
            callback:'show'//必须的 show为实参

        }).then(data =>{
            console.log(data)

        })
    </script>
</body>
</html>

后端配合(Node.js): 后端将会接受到前端返回的url,在url里面可以接收到前端传递来的callback回调函数,在返回数据时调用callback函数,即可

js 复制代码
// server.js - 原生 Node.js 版本
const http = require('http');

const server = http.createServer((req, res) => {
  // 匹配 GET 请求 /say
  if (req.url.startsWith('/say')) {
    // 解析查询参数(简单处理)
    const url = new URL(req.url, `http://${req.headers.host}`);
    console.log(url.searchParams)
    const wd = url.searchParams.get('wd');
    const callback = url.searchParams.get('callback');
    console.log(url)
    console.log(wd);      // Iloveyou
    console.log(callback); // show

    // 返回 JSONP 格式响应
    res.writeHead(200, { 'Content-Type': 'application/javascript' });
    const data = {
        code:0,
        msg:'字节,我来了'
    }
    res.end(`${callback}(${JSON.stringify(data)})`);

  } else {

    res.writeHead(404);
    res.end('Not Found');
  }
});

server.listen(3000, () => {
  console.log('Server running at http://localhost:3000');
});
JSONP的优缺点

优点

  • 兼容性好(支持IE6+等老浏览器)
  • 不需要后端特殊配置(CORS)

缺点

  • 只支持GET请求
  • 错误处理困难
  • 存在XSS风险(信任所有返回的JS)
  • 需要前后端约定回调参数名

2. CORS:现代跨域的"正规军"

CORS(Cross-Origin Resource Sharing)是现代浏览器支持的官方跨域方案。

工作原理

从源头解决问题,让fetch可以使用,使得数据可以跨域

当浏览器检测到跨域请求时:

  1. 对于简单请求(GET/POST/HEAD + 特定Header),直接发送请求
  2. 对于复杂请求(PUT/DELETE等),先发送OPTIONS预检请求
  3. 服务器响应是否允许跨域
  4. 浏览器根据响应决定是否放行实际请求
简单请求

直接在后端的请求头设置:'Access-Control-Allow-Origin'属性

  • 'Access-Control-Allow-Origin': '*'表示允许所有不同域的ip地址访问数据
  • 'Access-Control-Allow-Origin': 'http://localhost:3030'表示只允许http://localhost:3030 这个地址访问
js 复制代码
 if(req.url === '/api/test' && req.method === 'GET') {
        res.writeHead(200, {
            'Content-Type': 'application/json',
            'Access-Control-Allow-Origin': '*',

        })

        res.end(JSON.stringify({
            msg: '跨域成功!'
        }))
    }
复杂请求

由于请求较为复杂,可能会修改我的数据,不仅仅是简单的读取,所以需要更高的权限,因此要先发送OPTIONS预检请求,看是否符合条件,再向服务器发送真实的请求

后端CORS配置
js 复制代码
const http = require('http');

const server = http.createServer((req, res) => {
  // 设置CORS头
  res.setHeader('Access-Control-Allow-Origin', 'http://localhost:5173');
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
  res.setHeader('Access-Control-Allow-Credentials', 'true');
  
  // 处理预检请求
  if (req.method === 'OPTIONS') {
    res.writeHead(204); // No Content
    res.end();
    return;
  }
  
  // 处理实际请求
  if (req.url === '/api/data') {
    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ message: '跨域成功!' }));
  }
});

server.listen(8080, () => {
  console.log('CORS服务器运行在 http://localhost:8080');
});
CORS核心响应头
响应头 作用
Access-Control-Allow-Origin 允许的源(* 或特定域名)
Access-Control-Allow-Methods 允许的HTTP方法
Access-Control-Allow-Headers 允许的自定义请求头
Access-Control-Allow-Credentials 是否允许发送Cookie(true/false)
Access-Control-Max-Age 预检请求缓存时间(秒)
前端使用CORS
js 复制代码
// 带凭证的请求(如Cookie)
fetch('http://api.com/data', {
  credentials: 'include' 
})
.then(response => response.json())
.then(data => console.log(data));

其他跨域方案

  1. Nginx反向代理

    nginx 复制代码
    server {
      listen 80;
      server_name local.dev;
      
      location /api {
        proxy_pass http://localhost:8080;
        add_header Access-Control-Allow-Origin *;
      }
    }
  2. WebSocket:天然支持跨域

    js 复制代码
    const socket = new WebSocket('ws://api.com');
  3. postMessage:跨窗口通信

    js 复制代码
    // 发送方
    otherWindow.postMessage('Hello!', 'https://target.com');
    
    // 接收方
    window.addEventListener('message', event => {
      if (event.origin !== 'https://trusted.com') return;
      console.log('收到消息:', event.data);
    });

总结:如何选择跨域方案

方案 适用场景 特点
JSONP 老项目兼容、简单GET请求 兼容性好但安全性低
CORS 现代Web应用首选 官方标准、功能全面
反向代理 前端控制跨域、避免后端改动 部署时常用、配置灵活
WebSocket 实时双向通信 不受同源策略限制

实际开发建议:现代项目优先使用CORS,老项目或特殊场景考虑JSONP,部署时可通过Nginx统一处理跨域。

理解了跨域的原理和各种解决方案,下次面试官再问跨域问题,你就可以从容应对了!在实际项目中,根据需求选择最适合的方案,让数据自由穿越浏览器的"边防"吧!🚀


相关推荐
小楓12011 小时前
後端開發技術教學(三) 表單提交、數據處理
前端·后端·html·php
破刺不会编程1 小时前
linux信号量和日志
java·linux·运维·前端·算法
阿里小阿希1 小时前
Vue 3 表单数据缓存架构设计:从问题到解决方案
前端·vue.js·缓存
JefferyXZF2 小时前
Next.js 核心路由解析:动态路由、路由组、平行路由和拦截路由(四)
前端·全栈·next.js
汪子熙2 小时前
浏览器环境中 window.eval(vOnInit); // csp-ignore-legacy-api 的技术解析与实践意义
前端·javascript
还要啥名字2 小时前
elpis - 动态组件扩展设计
前端
BUG收容所所长2 小时前
🤖 零基础构建本地AI对话机器人:Ollama+React实战指南
前端·javascript·llm
鹏程十八少2 小时前
7. Android RecyclerView吃了80MB内存!KOOM定位+Profiler解剖+MAT验尸全记录
前端
小高0072 小时前
🚀前端异步编程:Promise vs Async/Await,实战对比与应用
前端·javascript·面试
Spider_Man2 小时前
"压"你没商量:性能优化的隐藏彩蛋
javascript·性能优化·node.js