Skip to content

跨域问题解析

1. 视频地址

本笔记由以下视频整理而来,感谢禹神的分享🌹

2. 什么是跨域问题?

首先需要明确一点,跨域问题是浏览器为了确保资源安全,遵循同源策略而引起的。那么什么是同源策略呢?源是由协议、域名和端口号这三者共同决定的,即:

txt
源 = 协议 + 域名 + 端口号

比如:

源A源B是否同源原因
http://www.xyz.com/homehttps://www.xyz.com/home不同源协议不同
http://www.xyz.com/homehttp://mail.xyz.com/home不同源域名不同
http://www.xyz.com:8080/homehttp://www.xyz.com:8081/home不同源端口号不同
http://www.xyz.com/homehttp://www.xyz.com/search同源协议、域名、端口号相同

同源请求与非同源请求:假设浏览器当前显示的网页处于源A,该网页需要发送一个请求到源B,如果源A和源B是同源的,那么称该请求是同源的,反之则是非同源的。非同源又称为跨域。

image-20241117225941989

3. 跨域的限制

浏览器对跨域有如下限制(假设源A和源B是非同源的):

  • 限制DOM访问:即源A不能读取和操作源B的DOM。
  • 限制Cookie访问:即源A不能访问源B的Cookie。
  • 限制Ajax获取数据:源A可以给源B发送请求,但是无法获取源B响应的数据。

在上述限制中,浏览器对Ajax获取数据的限制影响最大,也是我们在开发过程中需要解决的问题。

3.1 限制DOM访问

例如,在如下示例中,我们不能访问iframe中的DO M元素:

image-20241117232306949

3.2 限制Cookie访问

我们可以通过document.cookie来获取当前源的Cookie,但是,如果要获取不同源的Cookie值,由于在上一步中,获取异源的document已经失败了,所以不能获取异源的Cookie值。

3.3 限制Ajax获取数据

例如,在如下示例中,当我们发送不同源请求获取数据时,会失败:

image-20241118000549782

我们要注意的是,浏览器并不限制跨域请求,我们可以正常发出请求,然后服务器也能正常给出响应,但是浏览器在获取响应时,会进行校验,如果校验通过,则可以正常获取数据,如果校验不通过,则获取不到数据。所以问题的关键是如何让浏览器校验通过。

image-20241118001120128

4. 注意点

在解决跨域问题之前,有几个注意点需要我们关注,这是理解解决跨域问题方法的关键:

  • 跨域限制仅存在浏览器端,服务端不存在跨域限制;
  • 即使存在跨域,但是Ajax请求也是可以正常发出,但响应数据不会交给开发者(浏览器拦截了);
  • <link><script>img...等标签发出的请求也可能跨域,只不过浏览器对标签跨域不做严格限制,对开发几乎无影响;

5. 解决跨域问题的方法

5.1 CORS解决跨域问题

5.1.1 CORS概述

CORS全称是Cross-Origin Resource Sharing(跨域资源共享),是用于控制浏览器校验跨域请求的一套规范,服务器依照CORS规范,添加特定响应头来控制浏览器校验,大致规则如下:

  • 服务器明确表示拒绝跨域请求,或没有表示,则浏览器校验不通过;
  • 服务器明确表示允许跨域请求,则浏览器校验通过。

备注说明:使用CORS解决跨域是最正统的方式,且要求服务器是“自己人”。

CORS详解 | MDN

5.1.2 CORS解决简单请求跨域问题

首先利用express搭建一个服务,该服务运行http://127.0.0.1:8081源上:

js
const express = require('express');

const app = express()

const students = [
    {
        id: 1,
        name: 'John',
        age: 20
    },
    {
        id: 2,
        name: 'Mary',
        age: 21
    },
    {
        id: 3,
        name: 'Peter',
        age: 22
    }
]
app.get('/students', (req, res) => {
    res.send(students)
})

app.listen(8081)

然后编写页面:

html
<body>
    <button onclick="getStudents()">获取学生数据</button>
    <div id="students"></div>

    <script>
        async function getStudents() {
            const result = await fetch('http://127.0.0.1:8081/students')
            const data = await result.json()

            document.getElementById('students').innerHTML = JSON.stringify(data)
        }
    </script>
</body>

该页面运行在http://127.0.0.1:5500源上。

由于页面和服务位于不同的源上,所以此时页面获取不到数据。如果要让浏览器通过校验,那么就需要服务告诉浏览器:不要拦截这个请求。服务可以通过设置响应头Access-Control-Allow-Origin来实现这一点:

js
res.setHeader('Access-Control-Allow-Origin', 'http://127.0.0.1:5500')

服务可以指定特定的源,也可以使用通配符*来允许所有的源访问。

5.1.3 简单请求与复杂请求

CORS会把请求分为简单请求和复杂请求:

  • 简单请求(满足以下所有情况的称为简单请求):

    • 请求方法为GETHEADPOST

    • 请求头字段符合CORS安全规范

      简记:只要不手动修改请求头,一般都能符合该规范。

    • 请求头的Content-Type的值只能是以下三种

      • application/x-www-form-urlencoded
      • multipart/form-data
      • text/plain
  • 不是简单请求的就是复杂请求,复杂请求会自动发送预检请求(preflight)

关于预检请求:

  1. 发送时机:预检请求在实际跨域请求之前发出,是由浏览器自动发出的。

  2. 主要作用:用于向服务器确认是否允许接下来的跨域请求。

  3. 基本流程:先发起OPTIONS预检请求,如果浏览器通过预检请求,则继续发起实际的跨域请求。

  4. 请求头内容:一个OPTIONS预检请求,通常会包含以下请求头:

    请求头含义
    Origin发起请求的源
    Access-Control-Request-Method实际请求的HTTP方法
    Access-Control-Request-Headers实际请求中使用的自定义头(如果有的话)

5.1.4 CORS解决复杂请求跨域问题

首先改造HTML页面,使其发送

针对复杂请求,服务需要额外对OPTIONS预检请求进行处理,添加对应的响应头:

js
app.options('/students', (req, res) => {
    res.setHeader('Access-Control-Allow-Origin', 'http://127.0.0.1:5500')
    res.setHeader('Access-Control-Allow-Methods', 'GET,POST,DELETE')
    res.setHeader('Access-Control-Allow-Headers', 'Content-Type,xyz')
    res.setHeader('Access-Control-Max-Age', 7200)
    res.send()
})

响应头的含义如下:

响应头含义
Access-Control-Allow-Origin允许的源
Access-Control-Allow-Methods允许的方法
Access-Control-Allow-Headers允许的自定义头
Access-Control-Max-Age预检请求的结果缓存时间(单位:秒)

贴一下完整代码:

html
<body>
    <button onclick="getStudents()">获取学生数据</button>
    <div id="students"></div>

    <script>
        async function getStudents() {
            const result = await fetch('http://127.0.0.1:8081/students', {
                method: 'DELETE',  // 更改请求方法
                headers: {
                  	// 自定义请求头
                    'Content-Type': 'application/json',
                    'xyz': 'abc' 
                }
            })
            const data = await result.json()

            document.getElementById('students').innerHTML = JSON.stringify(data)
        }
    </script>
</body>
js
const express = require('express');

const app = express()

const students = [
    {
        id: 1,
        name: 'John',
        age: 20
    },
    {
        id: 2,
        name: 'Mary',
        age: 21
    },
    {
        id: 3,
        name: 'Peter',
        age: 22
    }
]
app.delete('/students', (req, res) => {
    res.setHeader('Access-Control-Allow-Origin', 'http://127.0.0.1:5500')
    res.send(students)
})
app.options('/students', (req, res) => {
    res.setHeader('Access-Control-Allow-Origin', 'http://127.0.0.1:5500')
    res.setHeader('Access-Control-Allow-Methods', 'GET,POST,DELETE')
    res.setHeader('Access-Control-Allow-Headers', 'Content-Type,xyz')
    res.setHeader('Access-Control-Max-Age', 7200)
    res.send()
})

app.listen(8081)

5.1.5 cors库解决跨域问题

首先通过以下命令安装cors:

bash
npm i cors

然后在代码中引入cors:

js
const express = require('express');
var cors = require('cors')  // 引入cors

const app = express()
app.use(cors()) // 使用cors

const students = [...] // 省略
app.delete('/students', (req, res) => {
    res.send(students)
})

app.listen(8081)

当然我们也可以配置cors,详细查看cors使用文档

5.2 JSONP解决跨域问题

5.2.1 JSONP概述

JSONP概述:JSONP是利用了<script>标签可以跨域加载脚本,且不受严格限制的特性。

基本流程:

  • 第一步:客户端创建一个<script>标签,并将其src属性设置为包含跨域请求的URL,同时准备一个回调函数,这个回调函数用于处理返回的数据。
  • 第二步:服务端接收到请求后,将数据封装在回调函数中并返回。
  • 第三步:客户端的回调函数被调用,数据以参数的形势传入回调函数。

image-20241119225310185

JSONP的限制是只能接受GET请求,因为<script>标签是GET请求发送的。

5.2.2 JSONP案例一

服务端代码:

js
const express = require('express');

const app = express();

app.get('/test', (req, res) => {
    res.send("alert('hello jsonp')")
})

app.listen(8081)

网页代码:

html
<body>
    <script src="http://127.0.0.1:8081/test"></script>
</body>

呈现效果:

image-20241119230133125

浏览器通过<script>标签给服务端发送请求,服务端响应内容,浏览器将响应内容当作JS代码执行,所以显示弹窗。

上面案例展示了JSONP的基本原理,但是实际开发中并不会这么使用,我们需要动态的执行回调函数。

5.2.3 JSONP案例二

服务端代码:

js
const express = require('express');

const app = express();

app.get('/teachers', (req, res) => {
    const { callback } = req.query
    const teachers = [
        { name: 'tom', age: 18 },
        { name: 'jack', age: 20 }
    ]
    res.send(`${callback}(${JSON.stringify(teachers)})`)
})

app.listen(8080)

网页代码:

html
<body>
    <button onclick="getTeachers()">获取教师信息</button>
    <script>
        function showTeachers(teachers) {
            console.log(teachers);

            alert(JSON.stringify(teachers));
        }

        function getTeachers() {
            var script = document.createElement('script');
            script.onload = function () {
                script.remove();
            }
            script.src = 'http://127.0.0.1:8080/teachers?callback=showTeachers';
            document.body.appendChild(script);
        }
    </script>
</body>

呈现效果:

image-20241119231729552

请求逻辑:点击网页上的按钮,执行函数getTeachers(),然后动态地在网页上添加<script>标签,该标签向服务端发送请求,并且向服务端指定了回调函数,服务端将数据和回调函数一起返回。网页得到响应,执行回调函数,最后将<script>标签删除,不影响网页结构。

5.2.4 jQuery封装JSONP

首先我们引入jQuery:

html
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.7.1/jquery.js"></script>

然后直接使用jQuery封装好的函数(注意callback是固定的):

html
<body>
    <button onclick="getTeachers()">获取教师信息</button>
    <script>
        function getTeachers() {
            $.getJSON('http://127.0.0.1:8080/teachers?callback=?', function (data) {
                alert(JSON.stringify(data));
            })
        }
    </script>
</body>

5.3 配置代理解决跨域问题

5.3.1 自己配置代理服务器

如果现在网页和服务端是同源的,但是网页想请求异源的资源,我们可以在服务端设置一个代理服务器,通过服务端转发请求解决跨域问题。

在服务端,我们可以安装http-proxy-middleware库来引入代理服务器:

bash
npm i http-proxy-middleware

服务端代码:

js
const express = require('express')
const { createProxyMiddleware } = require('http-proxy-middleware')
var path = require('path');

const app = express()

// 利用express.static中间件来托管静态资源。
app.use(express.static(path.join(__dirname, 'public')));

app.use('/api', createProxyMiddleware({
    target: 'https://www.toutiao.com',
    changeOrigin: true,
    pathRewrite: {
        '^/api': ''
    }
}))

app.listen(8081, () => {
    console.log('server is running at http://127.0.0.1:8081')
})

网页端代码:

html
<body>
    <button onclick="getNews()">获取新闻</button>
    <script>
        async function getNews() {
            let result = await fetch('http://127.0.0.1:8081/api/hot-event/hot-board/?origin=toutiao_pc')
            let data = await result.json()
            console.log(data)
        }
    </script>
</body>

效果如下:

image-20241120190758164

注意,使用这种方式,要确保服务与网页是同源的!

5.3.2 开发过程中vue配置代理服务器

在开发过程,我们也可以借助框架配置代理服务器。例如,使用Vite+vue开发项目过程中,我们可以在vite.config.js文件中添加如下配置:

js
server: {
    proxy: {
      '/api': {
        target: 'https://www.toutiao.com',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, ''),
      },
    }
  }

5.3.3 配置nginx作为代理服务器

我们可以通过配置nginx将我们的请求转发给异源服务:

json
server {
    listen       80;                                                         
    server_name  localhost;       

  	
    location /api/ {
  		  # 设置代理目标
        proxy_pass https://www.toutiao.com/; 
    }
}

注意:如果proxy_pass的路径最后没有 /,那么转发的路径将会带上/api