# 基于 Nignx 的前后端分离
# 1. 反向代理服务器
# 1.1 概念
由于请求的方向是从客户端发往服务端,因此 客户端 -> 服务端
这个方向是『正向』。
所谓『反向代理服务器』指的就是:Nginx『站』在服务端的角度,分担了服务端的负担,增强了服务端的能力。
在这种情况下,在客户端看来,
Nginx
+服务端
整体扮演了一个更大意义上的服务端的概念。
# 1.2 基于 Nginx 的动静分离方案
对 Nginx 的最简单的使用是将它用作静态资源服务器。
在这种方案种,将 .html
、.css
、.js
、.png
等静态资源放置在 Nginx 服务器上。
将对静态资源的访问流量就分流到了 Nginx 服务器上,从而减轻 Servlet 容器的访问压力。
# 1.3 基于 Nginx 的前后端分离
随着前端单页应用技术的发展,『前端』从简单的『前端页面』演进成了『前端项目』。
这种情况下,在动静分离方案的基础上进一步延伸出了『更激进』的方案:前后端分离。
# 1.4 实现原理
要实现前后端分离(涵盖动静分离),这里需要 Nginx 能提供一种能力:请求转发。
在整个过程中,所有的请求首先都是『交到了 Nginx 手里』,有一部分 请求是 Nginx 自己能响应的,它就响应了;而另一部分请求则是被 Nginx 转给了 SpringBoot,而等到 Nginx 获得到 SpringBoot 的 JSON 的返回之后,Nginx 再将响应数据回复给客户端。
# 2. Nginx 代理(转发)配置
提示
所谓『代理』指的就是 Nginx『帮』真正的服务端所接收的请求,那么也就意味着这样的请求,Nginx 最终需要再交给真正的服务端去处理。
# 2.1 两个需要提前交代的问题
“减法” 问题
在处理转发请求时,Nginx 常常对 URL 做一个 “减法” 操作,即,减去 URL 中的协议、IP 和端口部分,然后再使用剩下的部分。例如:
- URL
http://127.0.0.1:8080
做减法后啥,都不剩; - URL
http://127.0.0.1:8080/
做减法后,还剩/
; - URL
http://127.0.0.1:8080/api
做减法后,还剩/api
; - URL
http://127.0.0.1:8080/api/
做减法后,还剩/api/
。
- URL
“规则 2 选 1” 问题:
用户的原始 URL 会被 Nginx “加工” 成什么样子?请求会被转发到谁那里?有 2 套规则,具体是哪套规则起作用取决于你的 location 中的 proxy_pass 做 “减法” 后还剩不剩东西?例如
- URL 1:
http://127.0.0.1:8080
- URL 2:
http://127.0.0.1:8080/
- URL 3:
http://127.0.0.1:8080/api
- URL 4:
http://127.0.0.1:8080/api/
上面 4 个 URL ,后 3 个 URL 使用同一个规则,而第 1 个 URL 则使用的是另一个规则。
- URL 1:
# 2.2 两个 URL 处理规则
下述的
path
是用户请求的原始路径做 “减法” 之后剩下的内容。
Nginx 会使用两个 URL 处理规则的哪一个来处理用户请求?这取决于你的 proxy_pass
做 “减法” 之后还剩不剩东西。
规则一:如果『啥都不剩』,转发路径就是
proxy_pass
+path
规则二:如果『还剩东西』(哪怕就剩个
/
),转发路径是proxy_pass
+ (path
-location
)
补充两点:
无论如何配置你配置
proxy_pass
的内容最后一定会『完全地』包含在转发、去往的路径中。location 是否以
/
结尾问题不大,因为 Nginx 会认为/
本身就是 location 的内容本身(的一部分)。
两种 URL 处理规则的关键区别在于:你需不需要 Nginx 帮你从 URL 中截取掉一部分内容?如果不需要,那么就要配置成上述规则一的形式;如果需要,那么就要配置成上述规则二的形式。
# 2.3 示例
假设 Nginx 运行在 127.0.0.1 ,它所代理的目标服务在 192.172.0.x 上。
示例一:Nginx 接收到的请求是 127.0.0.1:8080/xxx/hello
location /xxx { proxy_pass http://192.172.3.110:8080; }
用户原始请求的 URL 做减法后的剩下的内容是:
/xxx/hello
。上述配置的 proxy_pass 做减法之后啥都不剩,因此使用上述规则一:
proxy_pass
+path
:http://192.172.3.110:8080 + /xxx/hello └──> http://192.172.3.110:8080/xxx/hello
最终 Nginx 会将请求发给
http://192.172.3.110:8080/xxx/hello
。示例二:Nginx 接收到的请求是 127.0.0.1/xxx/hello
location /xxx { proxy_pass http://192.172.3.110:8080/; }
用户原始请求的 URL 做减法后的剩下的内容是:
/xxx/hello
。上述配置的 proxy_pass 做减法之后还剩个
/
,因此使用上述规则二:proxy_pass
+ (path
-location
) 。http://192.172.3.110:8080/ + (/xxx/hello - /xxx ) └──> http://192.172.3.110:8080//xxx/hello
最终 Nginx 会将请求发给
http://192.172.3.110:8080/hello
。8080 后面的连续的两个//
,Nginx 会自己处理。示例二:Nginx 接收到的请求是 127.0.0.1/xxx/hello
location /xxx { proxy_pass http://192.172.3.110:8080/xxx/; }
用户原始请求的 URL 做减法后的剩下的内容是:
/xxx/hello
。上述配置的 proxy_pass 做减法之后还剩
/xxx/
,因此使用上述规则二:proxy_pass
+ (path
-location
) 。http://192.172.3.110:8080/xxx/ + (/xxx/hello - /xxx ) └──> http://192.172.3.110:8080//xxx/hello
最终 Nginx 会将请求发给
http://192.172.3.110:8080/xxx/hello
。8080 后面的连续的两个//
,Nginx 会自己处理。
# 3. Nginx 用于动静分离(了解)
为了显示明显的效果,准备两台独立的服务器:
Spring Boot(Thymeleaf)服务器。IP 地址为
81.68.200.174
。在本机(127.0.0.1)上运行 Nginx 。
# 3.1 Spring Boot 项目的内容和配置
SpringBoot 项目中提供动态的 thymeleaf 页面(这是动态页面,位于 template 目录下):
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8"/>
<title>Hello</title>
<script src="/js/jquery-1.11.3.js"></script>
<script type="text/javascript">
$(function() {
$('h2').text('hello world');
});
</script>
</head>
<body>
<h1 th:text="${message}">Hello World</h1>
</body>
</html>
thymeleaf 页面引用并使用了 jQuery,但是我们将项目中的 static
目录整体删除。即,Spring Boot 项目中并没有 jquery-1.11.3.js
文件。
Spring Boot 项目代码:
@RequestMapping("/api/welcome-page")
public String welcome(Model model) {
model.addAttribute("message", "http://www.baidu.com");
return "welcome";
}
直接运行并访问该 Spring Boot 项目,毫无疑问,你看不要页面上的 hello world 。
# 3.2 Nginx 配置
location .*\.js$ {
root html/js;
expires 30d;
}
location /api {
proxy_pass http://81.68.200.174:8080/api;
}
Nginx 的配置主要就是两个:
拦截以
.js
作为后缀的请求,并到指定的目录下查找、返回.js
文件。将接收到的以
/api
开始的请求,转向到81.68.200.174:8080
。
配置正确的情况下,通过 http://127.0.0.1/api/welcome-page
向 Nginx 发出请求,你看到页面,并且能够看到页面上的 hello world
。
# 4. 前后端分离及跨域问题
动静分离再『向前多走一步』,就是前后端分离。上例中的 Spring Boot 不提供任何动态页面、资源,只提供 JSON 格式数据。
将上例的 index.html 改造成如下形似:
<body>
<h2></h2>
<script src="./js/jquery-1.11.3.js"></script>
<script type="text/javascript">
$.ajax({
url: 'http://localhost:80/api/hello', // 注意这里的 URL
type: "POST",
success: function (result) {
$("h2").html("跨域访问成功:" + result.data);
},
error: function (data) {
$("h2").html("跨域失败!!");
}
});
</script>
</body>
再在 nginx 的 proxy_pass 配置成它所代理的 SpringBoot 的真实访问路径。例如:
location /api {
proxy_pass http://127.0.0.1:8080/api;
# proxy_set_header Host $http_host;
# proxy_set_header X-Real-IP $remote_addr;
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
简单起见,我们这里的 Spring Boot 就运行在本地,并占用 8080 端口。
在结合上述的配置,意味着我们在页面发起的 http://127.0.0.1:80/api/hello
的请求,被 Nginx 接收后,Nginx 会『帮』我们去访问 http://127.0.0.1:8080
的 /api/hello
,并将结果再返回给客户端了浏览器。
在这个过程中,客户端浏览器始终面对的都是 Nginx,因此,请求页面的 index.html
和 AJAX 请求 /api/hello
都是发往了同一个服务器,自然就没有跨域问题。
# 一个完整的 http 配置片段
其中绝大多数内容都是默认配置:
error_log logs/error.log info; # 打开错误日志的 INFO 级别,方便观察错误信息。
...
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65; # TCP 连接存活 65 秒
server {
# Nginx 监听 localhost:80 端口
listen 80;
server_name localhost;
# 访问 URI 根路径时,返回 Nginx 根目录下的 html 目录下的 index.html 或 index.htm
location / {
root html;
index index.html index.htm;
}
# URI 路径以 /api 开头的将转交给『别人』处理
location /api {
proxy_pass http://localhost:8080/api;
}
# 出现 500、502、503、504 错误时,返回 Nginx 根目录下的 html 目录下的 50x.html 。
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
}