写在前面

在前面我们学习了HTTP请求、响应报文、简单请求和非简单请求,接下来开始学习Nginx如何解决跨域问题。

简单请求和非简单请求

首先对前面学习的简单请求和非简单请求相关内容进行回顾。为什么需要区分简单请求和非简单请求,那是因为浏览器处理简单请求和非简单请求的方式不一样。

如果某个请求同时满足下面两个条件,那么该请求就属于简单请求:
(1)请求方法是GET、POST或者HEAD三者中的任意一个;
(2)HTTP头信息不超过下面几个字段:AcceptAccept-LanguageContent-LanguageLast-Event-IDContent-Type。请注意Last-Event-IDContent-Type只限于三个值application/x-www-form-urlencodedmultipart/form-datatext/plain

除此之外,凡是不同时满足这两个条件的,都属于非简单请求。

前面说过浏览器会针对这两种请求采取不同的处理方式,那么具体的处理方式又是怎样的呢?

简单请求

对于简单请求来说,浏览器会在其头信息中增加Origin字段后直接发出,Origin字段用来说明本次请求来自哪个源(协议+域名+端口)。

如果服务器发现Origin指定的源不在许可范围内,服务器会返回一个正常的HTTP回应,浏览器取到回应之后发现回应的头信息中没有包含Access-Control-Allow-Origin字段,就抛出一个错误给XHR的error事件;

如果服务器发现Origin指定的域名在许可范围内,那么服务器返回的响应会多出几个以Access-Control-开头的头信息字段。

非简单请求

说完了简单请求,接下来开始学习非简单请求,非简单请求是指对服务器有特殊要求的请求。举个例子,如请求方法是PUT、DELETE或者Content-Type值为application/json中的某种时,浏览器会在正式通信之前,发送一次HTTP预检OPTIONS请求,先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些 HTTP请求方法和头信息字段。只有得到肯定答复,浏览器才会发出正式的XHR请求,否则报错。

跨域

所谓的跨域请求是指在浏览器上当前访问的网站向另一个网站发送请求获取数据的过程。

跨域是浏览器的同源策略决定的,它是一个重要的浏览器安全策略,用于限制一个Origin的文档或者它加载的脚本与另一个源的资源进行交互,它能够帮助阻隔恶意文档,减少可能被攻击的媒介,如果开发者不需要跨域这个安全设置,那么可以使CORS配置来解除这个限制。

关于跨域大家可以参看MDN的 <浏览器的同源策略> 文档,这里仅仅列举几个同源和不同源的例子,来加深大家的理解和印象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 同源的例子
http://example.com/app1/index.html # 只是路径不同
http://example.com/app2/index.html

http://Example.com:80 # 只是大小写差异
http://example.com

# 不同源的例子
http://example.com/app1 # 协议不同
https://example.com/app2

http://example.com # host 不同
http://www.example.com
http://myapp.example.com

http://example.com # 端口不同
http://example.com:8080

跨域问题

为了解决跨域问题,前提是存在跨域问题,开发者可以依次按照如下步骤来进行操作。

一般跨域都是在前后端分离项目中存在的,因此这里通过使用Jquery提供的Ajax来向Nodejs启动的服务器发送请求来模拟跨域问题。

第一步,新建前端测试页面index.html,其中的内容如下,注意这里使用jQuery提供的ajax方法向后台接口请求数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Nginx反向代理(客户端)解决跨域</title>
</head>
<body>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<button id="testOne">发送测试请求1(客户端解决跨域问题)</button>
<button id="testTwo">发送测试请求2(客户端解决跨域问题)</button>
<script>
$(document).ready(function () {
$("#testOne").click(function () {
$.ajax({
url: "http://localhost:3000/jsons/one.json",
success (res) {
console.log("success",res)
},
error (err) {
console.log("error",err)
}
})
});
$("#testTwo").click(function () {
$.ajax({
url: "http://localhost:3000/jsons/two.json",
success (res) {
console.log("success",res)
},
error (err) {
console.log("error",err)
}
})
})
})
</script>
</body>
</html>

第二步,使用Nodejs第三方express框架来搭建服务器,模拟接口请求并获取json数据。
(1)安装nodejs,之后使用node -v命令来检测安装是否正常。
(2)使用如下命令来新建一个文件夹,并进入该目录:

1
2
mkdir express
cd express

(3)使用如下命令来安装Express:

1
cnpm install -g express-generator@4

(4)使用入门命令来创建一个工程:

1
express helloworld

这样在express文件夹内就出现了helloworld项目。
(5)安装helloworld项目依赖。进入helloworld项目,然后执行如下命令,它会将package.json文件中dependencies依赖列表中所有的依赖自动安装完成:

1
2
cd helloworld
cnpm install

(6)执行如下命令来启动服务:

1
cnpm start

(7)打开浏览器访问http://localhost:3000/,可以看到如下信息:

(8)打开app.js文件,找到第7和22行,可以看到如下代码:

1
2
var indexRouter = require('./routes/index');
app.use('/', indexRouter);

这就说明当开发者访问/链接的时候,会使用indexRouter,而indexRouter则是调用的是./routes/index.js文件,打开这个文件,可以看到如下的代码:

1
2
3
4
5
6
7
8
9
var express = require('express');
var router = express.Router();

/* GET home page. */
router.get('/', function(req, res, next) {
res.render('index', { title: 'Express' });
});

module.exports = router;

也就是说原来它会找views文件夹下的index.jade,该文件内的代码为:

1
2
3
4
5
extends layout

block content
h1= title
p Welcome to #{title}

该文件内的都是模板引擎写法,接下来尝试修改index.js文件中的内容如下所示:

1
2
3
4
5
6
7
8
9
var express = require('express');
var router = express.Router();

/* GET home page. */
router.get('/', function(req, res, next) {
res.render('index', { title: 'HelloWorld' });
});

module.exports = router;

之后重启项目,打开浏览器访问http://localhost:3000/,可以看到如下信息:

(9)访问静态html。首先查看app.js文件中的第20行代码:

1
app.use(express.static(path.join(__dirname, 'public')));

如果没有上述那句话就添加这句话,之后需要重启项目。

接着在项目的public文件夹下新建一个html文件夹,这样便于后期管理htmls静态页面,之后在该文件夹下新建一个index.html,其中的代码为:

1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>demo</title>
</head>
<body>
Hello World
</body>
</html>

之后保存退出,然后打开浏览器访问http://localhost:3000/htmls/index.html,可以看到如下信息:

(10)在项目的public文件夹下新建一个jsons文件夹,这样便于后期管理json数据,之后在该文件夹下新建一个one.json和一个two.json,其中的代码为:

1
2
3
4
5
6
7
8
9
10
11
# one.json
{
code: 200,
msg: "the method is one"
}

# two.json
{
code: 200,
msg: "the method is two"
}

第三步,以服务器方式打开前端index.html页面,注意此时前端index.html访问链接为http://localhost:63342/fontend/index.html,这个后续会使用到。然后点击发送测试请求1按钮,可以看到控制台输出如下信息:

由上图可知,当我们在本地63342端口向3000端口请求数据时就出现了跨域问题,此时就需要解决,这里选择采用Nginx反向代理来解决该问题。

Nginx反向代理(客户端)解决跨域

第四步,配置Nginx反向代理解决跨域问题。其实这里选择使用Nginx反向代理,其原理就是将前端和后端的地址使用Nginx转发到统一地址下,如将前端的63342和后端的3000端口转发到本地的5000端口下,这样就解决了跨域问题,具体的配置步骤如下所示:(请注意此时使用的Nginx是本机上的,而不是虚拟机上的

(1)在windows系统上D:\Application\Nginx-1.18.0\conf文件夹内新建vhost包,然后在nginx.conf文件内添加如下代码:

1
include vhost/*.conf;

(2)在D:\Application\Nginx-1.18.0\conf\vhost包内新建一个font.com.conf文件,里面的代码为:

1
2
3
4
5
6
7
8
9
10
11
12
server {
listen 5000;
server_name localhost;

location / {
proxy_pass http://localhost:63342/fontend/index.html;
}

location /jsons {
proxy_pass http://localhost:3000;
}
}

解释一下上述代码的含义,这里监听5000端口,服务名为localhost,也就是本机,然后第一个location匹配/ ,表示当请求http://localhost:5000/链接的时候,请求会被代理到http://localhost:63342/fontend/index.html页面,而这个不就前端页面的访问地址么:

第二个location匹配/jsons,请注意它不是精确匹配,因此像/jsons/one.json/jsons/one/one.json都是可以匹配到的 ,也就是说当开发者请求http://localhost:5000/jsons/one.json链接的时候,请求会被代理到http://localhost:3000/jsons/one.json;页面,而这个不就后端页面的访问地址么:

(3)在浏览器地址栏中访问http://localhost:5000,然后打开控制台的console,点击“发送测试请求1”和“发送测试请求2”按钮,可以看到控制台输出如下信息:

由于是同一个地址,因此就不存在跨域问题。

后端配置Nginx(服务端)解决跨域

除了采用Nginx的反向代理来解决跨域问题,其实还可以在后端配置Nginx。当浏览器在访问跨源的服务器时,也可以在跨域的服务器上直接设置Nginx,如将后端的3000端口转发到本地9000端口,并在Nginx中设置9000端口允许跨域,这样前端就可以无感开发,而不用将实际应当访问后端的地址修改为前端服务的地址,这样其实是最佳做法。

第一步,新建前端测试页面hello.html,其中的内容如下,注意这里使用jQuery提供的ajax方法向后台接口请求数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>服务端配置Nginx解决跨域</title>
</head>
<body>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<button id="testOne">发送测试请求1(服务端解决跨域问题)</button>
<button id="testTwo">发送测试请求2(服务端解决跨域问题)</button>
<script>
$(document).ready(function () {
$("#testOne").click(function () {
$.ajax({
url: "http://localhost:9000/jsons/one.json",
success (res) {
console.log("success",res)
},
error (err) {
console.log("error",err)
}
})
});
$("#testTwo").click(function () {
$.ajax({
url: "http://localhost:9000/jsons/two.json",
success (res) {
console.log("success",res)
},
error (err) {
console.log("error",err)
}
})
})
})
</script>
</body>
</html>

第二步,启动Express后端服务。注意这里使用前面创建的helloworld项目,并启动Express后端服务。

第三步,服务端配置Nginx解决跨域问题。往D:\Application\Nginx-1.18.0\conf\vhost\font.com.conf文件内新增一个server区块,里面的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
server {
listen 9000;
server_name localhost;

location /jsons {
proxy_pass http://localhost:3000;
}

# 指定允许跨域的方法,*表示所有
add_header Access-Control-Allow-Methods *;
# 预检命令的缓存,如果不缓存则每次会发送两次请求
add_header Access-Control-Max-Age 3600;
# 如果是携带cookie的请求,那么需要加上这个字段,并设置为true
add_header Access-Control-Allow-Credentials true;

# 表示允许这个跨域调用,后面填写客户端发送请求的域名和端口
# $http_origin动态获取客户端请求的域,这里不能使用*,因为携带cookie的请求是不支持*号
add_header Access-Control-Allow-Origin $http_origin;

# 表示请求头的字段,动态获取
add_header Access-Control-Allow-Headers $http_access_control_request_headers;

# OPTIONS预检命令,预检命令主要是针对非简单请求,预检命令通过时才发送请求
# 它用于检查请求的类型是不是预检命令
if ($request_method = OPTIONS){
return 200;
}
}

请注意,发送预检命令的是非简单请求,如果是简单请求且不携带cookie,那么其实只需配置如下两个字段即可:

1
2
3
4
5
6
# 指定允许跨域的方法,*表示所有
add_header Access-Control-Allow-Methods *;

# 表示允许这个跨域调用,后面填写客户端发送请求的域名和端口
# $http_origin动态获取客户端请求的域,这里不能使用*,因为携带cookie的请求是不支持*号
add_header Access-Control-Allow-Origin $http_origin;

通过服务端配置Nginx的方式来解决跨域问题,只需修改前端ajax中的接口,无需修改前端服务器地址,这在一定程度上提高了前端的开发效率。

有些开发者觉得这里为何使用9000端口代理3000端口,为什么不直接监听3000端口呢?那是因为3000端口不是Nginx监听的,是Express服务占用的,因此想利用Nginx的监听和解决跨域能力,就必须使用Nginx服务来监听端口和配置跨域。

这样关于Nginx解决跨域问题的学习就到此为止,后续学习其他知识。

参考文章:nginx通过CORS实现跨域,感谢大佬的技术解惑。