关于跨域

出于安全问题考虑, 浏览器都会有跨域限制, 但这个限制只在浏览器端, 你都不想浏览器在和你的服务器交互时还和别的服务器有联系吧.

试想这样一种场景, 服务器A通过一系列认证手段信任了你的浏览器, 你的浏览器也可以自由地和服务器A交换数据. 忽然你点击某个链接, 跳转到服务器B提供的网页中, 而该网页包含一段js代码, 是去服务器A中取数据的, 这种情况下就非常危险了, 如果没有跨域限制, 浏览器就会就会将服务器A发来的数据显示出来, 服务器B也可以轻松地从服务器A取得数据(这些数据本应通过认证才可以取得).

协议 域名 端口号有一个不同, 都是跨域, 注意二级域名不同也是跨域

跨域请求分类

  • 简单跨域请求
    • 请求方法是GET、HEAD或者POST,并且当请求方法是POST时,Content-Type必须是application/x-www-form-urlencoded, multipart/form-data或着text/plain中的一个值。
    • 请求中没有自定义HTTP头部(只有Accept、Content-Type、Accept-Language、Content-Language)
  • 非简单跨域请求(带预检的请求)
    • 简单跨域请求第一条的其他情况
    • 请求中包含自定义HTTP头部

对于简单跨域请求, 浏览器在请求头部增加origin字段, 值为js代码所在的域, 也就是上文中的服务器B, 服务器收到请求, 在响应头部增加Access-Control-Allow-Origin字段, 直接返回数据, 至于这个数据能否被js代码拿到, 全凭浏览器的判断, 如果响应头部Access-Control-Allow-Origin包含请求头部中的origin, 浏览器就会返回数据给js代码, 否则忽略该响应

image-20221004223034640

对于非简单跨域请求, 浏览器首先发送一个预检请求, 请求方法为options, 头部除了origin, 还有Access-Control-Request-HeadersAccess-Control-Request-Method:, 服务器会在响应头添加以下三个字段, 响应中不包含数据. 浏览器根据响应判断, 如果通过就正常发送请求, 否则直接不发送请求

image-20221004224840650

了解以上知识后, 就能很清晰地看到, 解决跨域可以从两大方面入手–浏览器 服务器

跨域实现

从浏览器入手

只要浏览器不进行跨域检查, 就可以实现跨域

1. 关闭浏览器跨域检查(最暴力的方式)

可以用命令行启动浏览器, 并加一些启动参数, 我在windows上没测试成功, 看到网上有博客说mac可以

当然这种方法不太实用, 也不安全, 不过在开发调试时使用还是挺方便的

2. script标签的jsonp

JSONP是JSON with Padding的略称。它是一个非官方的协议,它允许在服务器端集成Script tags返回至客户端,通过javascript callback的形式实现跨域访问(这仅仅是JSONP简单的实现形式)。–来源百度

实验

  • html服务在本机5500端口, 相当于浏览器正在和5500端口交互, 去请求8080端口的服务
1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<script src="http://127.0.0.1:8080/index.js" charset="utf-8"></script>
</head>
<body>
<script>
show()
</script>
</body>
</html>
  • 跨域资源文件在本机8080端口

当我们打开html, 控制台即可见到输出, 成功跨域

从服务端入手

既然浏览器的跨域检查无可避免, 那就设法让他的检查通过

其实原理就是在响应头部添加允许跨域的字段, 以nodejs为例, 下面这段代码等价于app.use(cors()), 只不过是做了一个封装, 让我们更简便地调用

1
2
3
4
5
6
7
8
app.all('*', (req, res, next) => {
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "X-Requested-With");
res.header("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS");
res.header("X-Powered-By", ' 3.2.1');
res.header("Content-Type", "application/json;charset=utf-8");
next();
});

1. 服务端配置允许跨域

本文章服务端使用nodejs, 其他语言也类似, springboot是通过添加注解@CrossOrigin

实验

  • html服务在本机5500端口, 相当于浏览器正在和5500端口交互, 去请求3000端口的服务
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
</head>
<body>
<script>
fetch("http://localhost:3000")
.then(res => res.json())
.then(data => { console.log(data) })
</script>
</body>
<script>
</script>
</html>
  • 服务端监听本机3000端口
1
2
3
4
5
6
7
8
9
const express = require('express')
const app = express()
// const cors = require('cors')
// app.use(cors({
// origin: 'http://localhost:5500',
// }))
app.get('/', (req, res) => {
res.json({ "name": "zq", "age": 18 })
}).listen(3000)

当我们打开html, 可以看到开头熟悉的报错. 将js文件中的注释打开(app.use(cors()), 允许所有origin跨域), 重新启动服务端, 即可解决
亲测localhost 和 127.0.0.1 不能混用, 也会存在跨域

2. nginx反向代理

相当于是做了一个跳板, 在代理服务器端设置允许跨域, 由服务器去请求目标服务器, 再返回给浏览器

  • html服务在本机5500端口, 相当于浏览器正在和5500端口交互, 去请求nginx监听的8080端口的服务
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
</head>

<body>
<script>
fetch("http://localhost:8080")
.then(res => res.json())
.then(data => { console.log(data) })
</script>
</body>
<script>
</script>

</html>
  • nginx监听8080端口, 将请求转发至服务端的3000端口

画线的头部可加可不加, 都可以实现跨域, 因为nginx已经帮我们配置好允许跨域

  • 服务端监听本机3000端口
1
2
3
4
5
6
const express = require('express')
const app = express()

app.get('/', (req, res) => {
res.json({ "name": "zq", "age": 18 })
}).listen(3000)

参考: B站视频: https://www.bilibili.com/video/BV1Ei4y1o7jK

[Http跨域时候预检没通过的几种原因 - jyLi - 博客园 (cnblogs.com)](https://www.cnblogs.com/linxingyun/p/6772937.html#:~:text=带预检 (Preflighted)的跨域请求需要浏览器在发送真实HTTP请求之前先发送一个OPTIONS的预检请求,检测服务器端是否支持真实请求进行跨域资源访问,真实请求的信息在OPTIONS请求中通过Access-Control-Request-Method,Header和Access-Control-Request-Headers Header描述,此外与简单跨域请求一样,浏览器也会添加Origin Header。)