Appearance
跨域问题解析
1. 视频地址
本笔记由以下视频整理而来,感谢禹神的分享🌹
2. 什么是跨域问题?
首先需要明确一点,跨域问题是浏览器为了确保资源安全,遵循同源策略而引起的。那么什么是同源策略呢?源是由协议、域名和端口号这三者共同决定的,即:
txt
源 = 协议 + 域名 + 端口号比如:
同源请求与非同源请求:假设浏览器当前显示的网页处于源A,该网页需要发送一个请求到源B,如果源A和源B是同源的,那么称该请求是同源的,反之则是非同源的。非同源又称为跨域。

3. 跨域的限制
浏览器对跨域有如下限制(假设源A和源B是非同源的):
- 限制DOM访问:即源A不能读取和操作源B的DOM。
- 限制Cookie访问:即源A不能访问源B的Cookie。
- 限制Ajax获取数据:源A可以给源B发送请求,但是无法获取源B响应的数据。
在上述限制中,浏览器对Ajax获取数据的限制影响最大,也是我们在开发过程中需要解决的问题。
3.1 限制DOM访问
例如,在如下示例中,我们不能访问iframe中的DO M元素:

3.2 限制Cookie访问
我们可以通过document.cookie来获取当前源的Cookie,但是,如果要获取不同源的Cookie值,由于在上一步中,获取异源的document已经失败了,所以不能获取异源的Cookie值。
3.3 限制Ajax获取数据
例如,在如下示例中,当我们发送不同源请求获取数据时,会失败:

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

4. 注意点
在解决跨域问题之前,有几个注意点需要我们关注,这是理解解决跨域问题方法的关键:
- 跨域限制仅存在浏览器端,服务端不存在跨域限制;
- 即使存在跨域,但是Ajax请求也是可以正常发出,但响应数据不会交给开发者(浏览器拦截了);
<link>、<script>、img...等标签发出的请求也可能跨域,只不过浏览器对标签跨域不做严格限制,对开发几乎无影响;
5. 解决跨域问题的方法
5.1 CORS解决跨域问题
5.1.1 CORS概述
CORS全称是Cross-Origin Resource Sharing(跨域资源共享),是用于控制浏览器校验跨域请求的一套规范,服务器依照CORS规范,添加特定响应头来控制浏览器校验,大致规则如下:
- 服务器明确表示拒绝跨域请求,或没有表示,则浏览器校验不通过;
- 服务器明确表示允许跨域请求,则浏览器校验通过。
备注说明:使用CORS解决跨域是最正统的方式,且要求服务器是“自己人”。
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会把请求分为简单请求和复杂请求:
简单请求(满足以下所有情况的称为简单请求):
请求方法为
GET、HEAD、POST请求头字段符合CORS安全规范。
简记:只要不手动修改请求头,一般都能符合该规范。
请求头的
Content-Type的值只能是以下三种application/x-www-form-urlencodedmultipart/form-datatext/plain
不是简单请求的就是复杂请求,复杂请求会自动发送预检请求(preflight)。
关于预检请求:
发送时机:预检请求在实际跨域请求之前发出,是由浏览器自动发出的。
主要作用:用于向服务器确认是否允许接下来的跨域请求。
基本流程:先发起
OPTIONS预检请求,如果浏览器通过预检请求,则继续发起实际的跨域请求。请求头内容:一个
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,同时准备一个回调函数,这个回调函数用于处理返回的数据。 - 第二步:服务端接收到请求后,将数据封装在回调函数中并返回。
- 第三步:客户端的回调函数被调用,数据以参数的形势传入回调函数。

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>呈现效果:

浏览器通过<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>呈现效果:

请求逻辑:点击网页上的按钮,执行函数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>效果如下:

注意,使用这种方式,要确保服务与网页是同源的!
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。