前后端联调——跨域通关秘宝:从JSONP到CORS

67 阅读7分钟

前后端联调时最常遇到的"拦路虎",我用两招轻松化解!

当浏览器变成"门卫大爷"

最近我在前后端分离项目中踩了个大坑:前端跑在http://localhost:5173,后端服务在http://localhost:8080。当我用fetch请求后端接口时,浏览器竟然亮出红牌警告

 useEffect(() => {
    (async () => {
      // 前后端联调
      const res = await fetch('http://localhost:8080/api/hello')
      const data = await res.json()
      console.log(data)
    })()

  }, [])

image.png

经过查阅资料:原来浏览器有个同源策略的安全机制,它要求前端只能访问同协议、同域名、同端口的资源。只要这三者任一不同,就被视为"跨域"操作。就像小区门卫只认本楼住户,外来人员一律拦截!浏览器执行跨域拦截主要是为了保护用户的网络安全和隐私,这种机制被称为同源策略(Same-Origin Policy)。同源策略是一个重要的安全概念,用于限制一个源的文档或脚本如何与另一个源的资源进行交互。它有助于隔离潜在的恶意网站,防止它们读取其他网站敏感数据。 举个例子:

protocol://domain:port 协议 域名 端口号

        domain 不一样 不是同一个来源
        http://localhost:5173 ->  http://www.baidu.com/api/user
        
        协议不同也不同源    严格 ? 为什么 ?
        http://localhost:5173 ->  https://localhost:5173/api/user
        
        端口不同也不同源  cross origin
        http://localhost:5173 ->  http://localhost:8080/api/user
        
        origin = http(s) + domain + port

那么我们怎么解决跨域问题呢,首先我们想到在前端imglinkscript好像都不受到同源策略的影响,都能进行跨域

img image.png

script

image.png

这些好像都是跨域进行跨域访问的,这些资源地址适合我们前端项目也是不同源的,由此,我们想到了第一种解决方案JSONP

初探跨域:JSONP的魔法

正当我抓狂时,发现了一个神奇的现象:虽然fetch请求被拦截,但HTML中的<script>标签却能畅通无阻地加载跨域资源!这不就是突破口吗?

JSONP的降龙十八掌

  1. 前端请求
    在页面声明回调函数,用script标签发起请求:

    <script>
    function callback(data) {
      console.log("收到数据:", data.msg); 
    }
    </script>
    <!-- 把回调函数名通过参数传给后端 -->
    <script src="http://localhost:8080/api/hello?callback=callback"></script>
    
  2. 后端响应
    后端返回包裹着函数调用的JS代码:

    // server.js
    res.writeHead(200, {'Content-Type': 'text/javascript'});
    const data = { msg: '字节我来了' };
    res.end("callback(" + JSON.stringify(data) + ")");
    
  3. 执行效果
    页面加载该脚本时自动执行:callback({msg: "字节我来了"}),完美实现跨域数据传输!

image.png

JSONP 利用了 script 可以跨域访问,前端使用script src=url跨域的资源请求地址需要后端配合,返回的JSON 外面包含着函数,页面上有个函数在等待执行,但是有没有感觉有点麻烦,每次都要创建一个函数,我们这时候就想到了封装一个JSONP函数

封装JSONP利器

每次手动创建script标签太麻烦?看我封装的神器:

后端代码

const http = require('http');

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

        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');
});

前端代码

<script>
function getJSONP({ url, params = {}, callback }) {
            // 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] = function (data) {
                    resolve(data)
                }

                document.body.appendChild(script)
            })
        }
        // 测试案列
        getJSONP({
            url: 'http://localhost:3000/say',
            params: {  // 查询参数
                wd: 'I love you'
            },
            callback: 'show'   // 必须的
        }).then(data => {
            console.log(data);
        })
</script>

image.png

  • 动态创建 <script> 标签,src 指向后端接口,并带上 ?callback=show
  • 在window全局创建callback函数
  • 后端接收到请求后,读取 callback 参数,把数据包装成该函数的调用返回
  • 浏览器加载脚本,执行 show({...}),从而拿到数据

JSONP的七寸软肋

  1. 只支持GET请求:无法发送POST/PUT等复杂请求
  2. 安全隐患:动态创建的回调函数暴露在全局,可能被恶意利用,比如xss跨站脚本攻击、CSRF跨站请求伪造
  3. 错误处理难:无法精确捕获网络错误

某次我将回调函数名拼写错误,调试两小时才发现——这坑踩得真疼!

所以有更方便、更安全的方法横空出世

进阶秘籍:CORS跨域神功

既然JSONP有局限,我继续探索更强大的解决方案——CORS(跨域资源共享) 。它的核心思想是:"我开放,你随意!"

简单请求

对于GET/POST等简单请求,后端只需设置一个响应头:

  fetch('http://localhost:8000/api/test')
             .then(res => res.json())
             .then(data => {
                 console.log(data);
             })
if (req.url === '/api/test' && req.method === 'GET') {
        res.writeHead(200, {
            'Content-Type': 'application/json',
            'Access-Control-Allow-Origin': '*'// 允许所有域名访问,当然也可以设置具体的白名单
        })

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

设置后,浏览器就会放行跨域请求,就像拿到了特别通行证!

image.png

复杂请求

可以看到有两次test请求,一个是预检请求 image.png

当我尝试发送PATCH请求时,发现浏览器先发了OPTIONS请求探路。这就引出了CORS的预检机制:

  1. 前端发起PATCH请求

    fetch('http://localhost:8000/api/test', {
      method: 'PATCH'
    })
    
  2. 浏览器自动发送OPTIONS预检

    OPTIONS /api/test HTTP/1.1
    Origin: http://127.0.0.1:5500
    Access-Control-Request-Method: PATCH
    

预检请求是用来检测后端是否允许这种复杂的PATCH请求,如果不允许,则真正的PATCH的请求就不会发起

  1. 后端响应放行

    if (req.method === 'OPTIONS') {
      res.setHeader('Access-Control-Allow-Origin', 'http://127.0.0.1:5500');//设置允许请求的地址
      res.setHeader('Access-Control-Allow-Methods', 'PUT,PATCH,GET,POST,DELETE');//设置允许请求的方式两者必须用时满足
      res.writeHead(200);
      res.end();
      return;
    }
    
  2. 正式请求通行

    if (req.url === '/api/test' && req.method === 'PATCH') {
      res.setHeader('Access-Control-Allow-Origin', 'http://127.0.0.1:5500');
      res.end(JSON.stringify({ msg: '跨域成功!!!' }));
    }
    

原理揭秘:浏览器相当于严谨的安检员,遇到非常规请求(如PATCH)时,会先问服务器:"这位乘客能带PATCH这个'行李'吗?" 获得许可后才放行正式请求。

血泪经验总结

方案适用场景优势致命缺陷
JSONP老旧浏览器兼容无需后端大改仅支持GET,安全性低
CORS现代Web应用支持所有HTTP方法需后端配合设置响应头

经过反复踩坑验证,我的最佳实践是:

  1. 优先使用CORS:主流方案,安全灵活

  2. 严格设置白名单:避免使用Access-Control-Allow-Origin: *

    // 只允许特定域名访问
    res.setHeader('Access-Control-Allow-Origin', 'https://trusted-site.com');
    
  3. 预检请求缓存:减少OPTIONS请求次数

    res.setHeader('Access-Control-Max-Age', '86400'); // 缓存24小时
    

终极灵魂拷问

某次面试官突然问我:"为什么CORS能解决跨域,但后端还是收到了请求?" 真相是:

  1. 请求能到达服务器:跨域限制是浏览器的行为
  2. 响应被浏览器拦截:没有正确CORS头部的响应会被丢弃
  3. 服务器日志有记录:即使前端报错,后端也能看到请求日志

我们的请求依然会到达服务器,但是响应结果会被服务器拦截

这就好比快递能送到小区门卫(服务器),但门卫发现收件人不在本楼(跨域),直接拒收了包裹(响应)!

总结

从被CORS拦路时的崩溃,到掌握两大解决方案的从容,我深刻体会到:

  1. 浏览器安全机制是双刃剑:没有同源策略,CSRF攻击将泛滥成灾
  2. 理解原理比死记配置更重要:明白OPTIONS预检的触发条件,才能快速定位问题
  3. 真实项目要综合考量:对第三方老接口可用JSONP过渡,内部系统首选CORS

最后送上我的调试秘籍:在Chrome地址栏输入chrome://flags/#block-insecure-private-network-requests并禁用该选项,可解除本地开发时的CORS限制(仅限开发环境!)。

跨域这座大山,终于被我挖通了隧道!下次遇到时,你就有两种选择方案应对