(笔记)什么是跨域?

590 阅读6分钟

1. 什么是跨域?

想象一下,你住在一个小区里,小区有门禁系统,只允许本小区的住户自由进出。如果外来的访客想进入,需要登记或获得许可。浏览器的跨域问题,就是这个“门禁系统”在起作用!

浏览器的安全策略规定:不同源的网站之间,默认不能互相访问数据(比如读取页面内容、发送请求等)。这是为了防止恶意网站窃取用户隐私(比如你的登录信息)。

2. 什么是同源策略?

“同源”是指两个网页的以下三个部分完全一致:

  1. 协议(如 http vs https
  2. 域名(如 example.com vs api.example.com
  3. 端口(如 80 vs 8080

例子:

  • https://www.example.com 和 http://www.example.com → 不同源(协议不同)
  • https://example.com 和 https://www.example.com → 不同源(域名不同)
  • https://example.com:80 和 https://example.com:8080 → 不同源(端口不同)

同源策略

 1. 浏览器出于安全考虑(数据安全,服务器安全,减少XSSCSRF攻击),
 2. http://    www.    abc.com :    8000    / a.html
     协议       子域名      主域名     端口       路径
 3. 非同源请求发送后,浏览器会拦截响应  

3. 跨域的方案

3.1 jsonp:

  1. 借助script 标签src属性不受同源策略的限制,来发送请求
  2. 携带一个参数callback给到后端
  3. 后端将数据作为callback函数的实参,返回给前端一个callback的调用形式
  4. 浏览器接收到callback的调用会自动执行全局的callback函数

代码演示:

前端代码:

<body>
    <button onclick="handle()">请求</button>  <!-- 点击按钮触发请求 -->
    <img src="" alt="">  <!-- 示例图片,与 JSONP 无关 -->

    <script>
        // JSONP 封装函数
        function jsonp(url, cb) {
            return new Promise((resolve, reject) => {
                // 1. 创建一个 <script> 标签
                const script = document.createElement('script');
                
                // 2. 在 window 对象上挂载一个以 cb 命名的回调函数
                // 后端返回的数据会触发此函数
                window[cb] = function (data) {
                    resolve(data); // 将后端返回的数据传递给 Promise 的 resolve
                    // 可选:请求完成后移除 script 标签和 window 上的回调函数
                    script.remove();
                    delete window[cb];
                }

                // 3. 设置 script 的 src,拼接回调函数名到 URL 参数中
                // 例如:http://localhost:3000?cb=callback
                script.src = `${url}?cb=${cb}`;

                // 4. 将 script 标签插入页面,触发请求
                document.body.appendChild(script);
            })
        }

        // 按钮点击事件处理函数
        function handle() {
            // 调用 jsonp 函数,传入后端地址和回调函数名
            jsonp('http://localhost:3000', 'callback').then(res => {
                console.log(res); // 输出后端返回的数据:hello world
            })
        }
    </script>
</body>

后端代码

const http = require('http');

// 创建 HTTP 服务器
http.createServer((req, res) => {
    // 解析请求的 URL,获取查询参数
    const url = new URL(req.url, `http://${req.headers.host}`);
    const query = url.searchParams;

    // 检查是否存在 'cb' 参数(前端传递的回调函数名)
    if (query.get('cb')) {
        const cb = query.get('cb');  // 获取回调函数名,这里是 'callback'
        const data = 'hello world';  // 要返回的数据
        
        // 拼接一个函数调用的字符串,例如:callback("hello world")
        const result = `${cb}("${data}")`;

        // 设置响应头(可选,但建议设置 Content-Type)
        res.setHeader('Content-Type', 'application/javascript');
        
        // 返回结果,前端会将其作为 JS 代码执行
        res.end(result);
    }

}).listen(3000);  // 监听 3000 端口

代码执行流程

  1. 前端点击按钮,调用 handle() 函数。

  2. 前端发起 JSONP 请求

    • 创建一个 <script> 标签,设置 src 为 http://localhost:3000?cb=callback
    • 定义全局函数 window.callback,用于接收数据。
  3. 后端收到请求

    • 解析 URL,获取 cb=callback
    • 返回字符串 callback("hello world")
  4. 前端执行返回的 JS 代码

    • 自动调用 window.callback("hello world")
    • Promise 的 resolve 被触发,数据传递给 then 中的回调函数。
  5. 控制台输出hello world

缺点:

  • 必须要前后端配合
  • 只能发送get请求
  • 不安全,容易受到xss攻击

3.2 CORS(跨域资源共享) (主流方案)

  • 原理:服务器明确告诉浏览器“允许哪些来源访问我”。
  • 实现:后端在响应头中添加字段,例如:
Access-Control-Allow-Origin: http://my-site.com  // 允许特定来源
Access-Control-Allow-Origin: *                   // 允许所有来源(慎用!)
  • 适用场景:前后端分离开发、调用第三方 API。

代码演示

const http = require('http');

const server = http.createServer((req, res) => {
    res.writeHead(200, {
        'access-control-allow-origin': 'http://127.0.0.1:5500', // 允许所有域名跨域
    })
    res.end('hello world');
})

server.listen(3000)

3.3 nginx 反向代理

  • 原理:让同源的服务器替你转发请求。

    • 你的前端请求 http://my-site.com/proxy-api
    • 你的服务器(my-site.com)转发请求到 http://api.com,返回结果给前端。
  • 适用场景:前端开发时可用 webpack-dev-server 的代理功能;生产环境用 Nginx 反向代理。

3.4 websocket

  1. 传统的前后端通讯是基于http协议的,是单向的,只能从一端发到另一端,无法双向通信
  2. websocket 是基于tcp协议,是双向的,可以从一端发送到另一端,也可以从另一端发送到一端
  3. socket协议一旦建立连接,就可以一直保持通信状态,不需要每次都建立连接
  4. 天生就可以跨域

前端代码

<body>
    <script>
        // WebSocket 测试函数
        function webSocketTest(url, params = {}) {
            return new Promise((resolve, reject) => {
                // 1. 创建一个 WebSocket 实例,连接到指定的 URL
                const socket = new WebSocket(url);

                // 2. 监听 WebSocket 连接成功事件
                socket.onopen = () => {
                    // 连接成功后,发送数据到服务器
                    socket.send(JSON.stringify(params)); // 将参数对象转为 JSON 字符串发送
                };

                // 3. 监听 WebSocket 接收到消息的事件
                socket.onmessage = (event) => {
                    console.log(event.data); // 打印服务器返回的数据

                    // 将接收到的数据传递给 Promise 的 resolve
                    resolve(event.data);
                };

                // 4. 监听 WebSocket 错误事件
                socket.onerror = (error) => {
                    reject(error); // 将错误传递给 Promise 的 reject
                };

                // 5. 监听 WebSocket 关闭事件
                socket.onclose = () => {
                    console.log('WebSocket 连接关闭');
                };
            });
        }

        // 调用 WebSocket 测试函数
        webSocketTest('ws://localhost:3000', { age: 18 }).then(res => {
            console.log(res); // 打印服务器返回的数据
        });
    </script>
</body>

后端代码:

const WebSocket = require('ws'); // 引入 WebSocket 库

// 在 3000 端口上创建 WebSocket 服务器
const ws = new WebSocket.Server({ port: 3000 });

let count = 0; // 定义一个计数器变量

// 监听客户端连接事件
ws.on('connection', (obj) => {
    // obj 是当前连接的客户端对象

    // 监听客户端发送的消息
    obj.on('message', (msg) => {
        // msg 是客户端发送的消息(Buffer 类型)
        console.log(msg.toString()); // 将消息转为字符串并打印

        // 向客户端发送一条消息
        obj.send('收到了');

        // 设置一个定时器,每隔 2 秒向客户端发送一次计数器的值
        setInterval(() => {
            count++; // 计数器自增
            obj.send(count.toString()); // 将计数器的值转为字符串并发送
        }, 2000);
    });

    // 监听客户端断开连接事件
    obj.on('close', () => {
        console.log('客户端断开连接');
    });
});

代码执行流程

  1. 前端调用 webSocketTest 函数

    • 创建一个 WebSocket 实例,连接到 ws://localhost:3000
    • 连接成功后,发送数据 { age: 18 } 到服务器。
  2. 后端接收到连接

    • 监听 connection 事件,获取客户端对象 obj
    • 监听 message 事件,接收客户端发送的消息,并打印 { age: 18 }
    • 向客户端发送一条消息 '收到了'
    • 启动定时器,每隔 2 秒向客户端发送一次计数器的值。
  3. 前端接收到消息

    • 打印服务器返回的消息('收到了' 和计数器的值)。
    • 将消息传递给 Promise 的 resolve,触发 then 中的回调函数。

关键点总结

  • WebSocket 特点

    • 全双工通信:客户端和服务器可以同时发送和接收数据。
    • 长连接:连接建立后,客户端和服务器可以持续通信。
    • 适合实时应用:如聊天室、实时数据推送等。
  • 前端核心

    • new WebSocket(url):创建 WebSocket 实例。
    • socket.send(data):发送数据。
    • socket.onmessage:接收数据。
  • 后端核心

    • new WebSocket.Server({ port }):创建 WebSocket 服务器。
    • ws.on('connection'):监听客户端连接。
    • obj.on('message'):接收客户端消息。
    • obj.send(data):向客户端发送数据。

3.5 postMessage

当父级页面和iframe页面不在同一个域名下,它们之间的数据传输也存在跨域问题,父级页面和iframe页面之间通过postMessage来通信

3.6 document.domain

同上(谷歌禁止)

4. 总结

  • 跨域是浏览器的安全机制,防止恶意网站窃取数据。
  • 同源要求协议、域名、端口一致
  • 常用解决方案:CORS(后端设置响应头)、代理服务器、JSONP(已过时)。