# 基于 Nignx 的前后端分离

# 1. 反向代理服务器

# 1.1 概念

由于请求的方向是从客户端发往服务端,因此 客户端 -> 服务端 这个方向是『正向』。

所谓『反向代理服务器』指的就是:Nginx『站』在服务端的角度,分担了服务端的负担,增强了服务端的能力。

在这种情况下,在客户端看来,Nginx + 服务端 整体扮演了一个更大意义上的服务端的概念。

nginx-01

# 1.2 基于 Nginx 的动静分离方案

对 Nginx 的最简单的使用是将它用作静态资源服务器。

nginx-02

在这种方案种,将 .html.css.js.png 等静态资源放置在 Nginx 服务器上。

将对静态资源的访问流量就分流到了 Nginx 服务器上,从而减轻 Servlet 容器的访问压力。

# 1.3 基于 Nginx 的前后端分离

随着前端单页应用技术的发展,『前端』从简单的『前端页面』演进成了『前端项目』。

这种情况下,在动静分离方案的基础上进一步延伸出了『更激进』的方案:前后端分离。

nginx-03

# 1.4 实现原理

要实现前后端分离(涵盖动静分离),这里需要 Nginx 能提供一种能力:请求转发。

在整个过程中,所有的请求首先都是『交到了 Nginx 手里』,有一部分 请求是 Nginx 自己能响应的,它就响应了;而另一部分请求则是被 Nginx 转给了 SpringBoot,而等到 Nginx 获得到 SpringBoot 的 JSON 的返回之后,Nginx 再将响应数据回复给客户端。

# 2. Nginx 代理(转发)配置

提示

所谓『代理』指的就是 Nginx『帮』真正的服务端所接收的请求,那么也就意味着这样的请求,Nginx 最终需要再交给真正的服务端去处理。

# 2.1 两个需要提前交代的问题

  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/
  2. “规则 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 则使用的是另一个规则。

# 2.2 两个 URL 处理规则

下述的 path 是用户请求的原始路径做 “减法” 之后剩下的内容。

Nginx 会使用两个 URL 处理规则的哪一个来处理用户请求?这取决于你的 proxy_pass 做 “减法” 之后还剩不剩东西。

  • 规则一:如果『啥都不剩』,转发路径就是 proxy_pass + path

  • 规则二:如果『还剩东西』(哪怕就剩个 /,转发路径是 proxy_pass + (path - location)

补充两点:

  1. 无论如何配置你配置 proxy_pass 的内容最后一定会『完全地』包含在转发、去往的路径中。

  2. 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/hello8080 后面的连续的两个 //,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/hello8080 后面的连续的两个 //,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 的配置主要就是两个:

  1. 拦截以 .js 作为后缀的请求,并到指定的目录下查找、返回 .js 文件。

  2. 将接收到的以 /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;
        }
    }
}