JS面试题-JSONP解决跨域问题

213 阅读4分钟

跨域和同源策略

什么是跨域

  • 当一个页面访问另一个页面时,协议(protocol)、域名(Domain)以及端口(port)任意一个不同时都是跨域,会受到浏览器同源策略的限制。

image.png

  • 同源

    • http://example.com:8080/ 和 http://example.com:8080/api/data 是同源的。
  • 跨域

    • http://example.com:8080/ 和 https://example.com:8080/ 是跨域的(不同协议)。
    • http://example.com:8080/ 和 http://api.example.com:8080/ 是跨域的(不同域名)。
    • http://example.com:8080/ 和 http://example.com:9090/ 是跨域的(不同端口)。
    • http://example.com:8080/ 和 https://api.anotherdomain.com:9090/ 是跨域的(不同协议、域名和端口)。

什么是同源策略

同源策略(Same-Origin Policy)是一种浏览器安全机制,旨在防止一个网页的脚本非法访问另一个网页的资源,特别是当这两个网页来自不同的源时。
举个简单的例子: 假设你在浏览一个银行网站 https://bank.example.com,该网站有一些敏感信息,比如账户余额和个人资料。现在有以下几种情况:

  • 同源请求:如果你点击了银行网站上的链接或者提交了一个表单,这些操作都是在同一个网站内部进行的,相当于你让自己的家人帮忙做事,浏览器会允许这种交互。
  • 跨源请求:如果有一个恶意网站 http://malicious-site.com 尝试通过JavaScript代码获取你在银行网站上的个人信息,这就像是一个陌生人试图闯入你家偷东西。为了保护你,浏览器会根据同源策略阻止这个请求,不让恶意网站访问银行网站的数据。\

跨域请求处理

  • 简单请求:浏览器会直接发送请求,但如果没有适当的 CORS 头部,JavaScript 将无法读取响应数据。
  • 非简单请求:浏览器会在实际请求之前发送预检请求,以确保服务器同意该类型的跨域请求。只有预检成功后,才会发出实际请求。

什么是JSONP?

JSONP(JSON with Padding)是一种非正式的,轻量级的传输协议,它允许网页通过 <script> 标签向不同域名下的服务器发送GET请求,并接收数据作为JavaScript回调函数的参数。其核心思想是利用 <script> 标签src属性不受同源策略限制的特点,动态创建 <script> 元素来加载外部资源,从而绕过浏览器的安全限制。

  • JSON with Padding ,填充式JSON,服务端不再只返回JSON格式的数据,而是返回一段调用某个函数的js代码,在src中进行了调用,这样实现了跨域。
  • <img src="xxx"> <link href="xxx">也不受同源策略的限制,但是只有script可以返回可以执行的脚本

JSONP的工作原理

  1. 客户端发起请求

    • 在前端页面中,定义一个全局回调函数,用于处理接收到的数据。
    • 动态创建一个 <script> 标签,并设置其 src 属性为包含回调函数名的URL。
    • 将这个 <script> 标签插入到DOM中,触发浏览器加载指定的URL。
  2. 服务器端响应

    • 服务器接收到请求后,解析出回调函数名,并根据该名称构建一段JavaScript代码。
    • 这段代码通常是一个函数调用,传递给回调函数的数据作为参数。
    • 服务器将这段JavaScript代码作为响应返回给客户端。
  3. 客户端执行回调

    • 浏览器下载并执行返回的JavaScript代码,调用之前定义好的全局回调函数。
    • 回调函数处理传入的数据,更新DOM或其他操作。

实例说明

后端node代码:

// http 服务启动
// 内置的http模块 
// commonjs node早期  es6模块化  
const http = require('http');
// 启动http服务
// 基于请求/响应的简单协议
const server = http.createServer((req, res) => {
  const users =[{
    id: 1,
    name: 'zs'
  },{
    id: 2,
    name: 'ls'}
]
// console.log(JSON.stringify(users))
  res.end(`callback(${JSON.stringify(users)})`)
})

server.listen(3000, () => {
  console.log('server is running at port 3000')
})

不单独返回JSON数据,而是会返回一段JavaScript代码,这段代码实际上是一个函数调用,传递给 callback 函数作为参数的数据就是JSON数据。

前端代码:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <ul id="list">

    </ul>
    <script>
        let list = document.getElementById('list');
        // fetch("http://127.0.0.1:3000")  // 跨域请求
        let jsonp = (url, callback) => {
            let oScript = document.createElement('script');
            oScript.src = url;
            document.body.appendChild(oScript);
            window.callback = callback
        }

        jsonp("http://localhost:3000",(data) => {
                list.innerHTML = data.map(item => `<li>${item.name}</li>`).join('');
                window.callback = null;
            })

    </script>
</body>
</html>

在JSONP中,前端通过创建一个 <script> 标签来请求数据,服务器响应的内容是一个JavaScript函数调用,这个函数名通常是前端指定的回调函数名。由于 <script> 标签加载的代码会在全局作用域中执行,因此回调函数必须是全局可见的,即挂在 window 对象上,这样才能确保当服务器返回的数据被执行时,浏览器能够找到并正确调用该回调函数。

默认情况下,document.body.appendChild(oScript) 会将新创建的 <script> 标签插入到 <body> 元素的末尾,即在所有已有子元素之后。

由于script 是阻塞式加载并执行的,如果不在最后插入的话,会造成callback函数未定义,在全局作用域中找不到此函数

image.png

JSONP的局限性

  • 仅支持GET请求:由于依赖于 <script> 标签,JSONP只能发送GET请求,无法处理POST等其他HTTP方法。
  • 安全性问题:如果服务器不可信,可能会导致恶意代码被执行,存在安全隐患。
  • 缺乏错误处理机制:JSONP没有内置的错误处理功能,一旦请求失败,很难捕获和处理错误。
  • 全局污染:每次请求都需要挂载一个全局回调函数,容易造成全局命名空间的污染。