背景信息
最近开始学习一个新的项目,读到此项目 Nginx 服务器的配置文件 default.conf,它通过auth_request模块,实现了根据子请求结果进行对访问用户身份验证。
bash
server {
listen 80;
listen [::]:80;
server_name localhost;
#access_log /var/log/nginx/host.access.log main;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
}
location /api/ { # 处理 /api开头的请求
auth_request /auth; # 发送一个子请求到 /auth 地址,用于验证请求的认证状态,也就是依赖另一个请求
proxy_pass http://localhost:8080/success; # 当有请求发送到 /api 路径时,Nginx 会将这些请求转发到在本机 12345 端口上监听的服务
}
location = /auth { # 有等号表示精确匹配,没有表示前缀匹配
internal; # 表示这个位置是内部的,意味着这个路径不能直接通过客户端请求访问,只能通过 Nginx 的内部机制,如 auth_request 指令,进行访问。
# 设置参数
set $query '';
if ($request_uri ~* "[^?]+?(.*)$") {
set $query $1;
}
access_log /var/log/nginx/auth_access.log;
error_log /var/log/nginx/auth_error.log;
# 验证成功,返回200 OK
proxy_pass http://localhost:8080/verify?$query;
# 发送原始请求
proxy_pass_request_body off;
# 清空 Content-Type
proxy_set_header Content-Type "";
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
还有一个后端控制器小 demo:
typescript
@RestController
@SpringBootApplication
public class Application {
private Logger logger = LoggerFactory.getLogger(Application.class);
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
@GetMapping("/verify")
public ResponseEntity<String> verify(String token) {
logger.info("接收到的token为:{}",token);
if ("success".equals(token)) {
logger.info("token验证成功",token);
return ResponseEntity.status(HttpStatus.OK).build();
} else {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
}
@GetMapping("/success")
public String success() {
return "xess is so bad";
}
}
我开始简单的实现:我打算将 Nginx 服务器运行在本机的 docker,将这个后端程序运行在本机,然后通过本机浏览器访问 /api 接口进行测试。
首先启动了一个名为 Nginx 的 Nginx Docker 容器,使其在后台运行,并将主机的 80 端口映射到容器的 80 端口,同时设置容器在任何情况下退出后都会自动重启,确保服务持续可用。
diff
docker run \
--restart always \
--name Nginx \
-d \
-p 80:80 \
nginx
为了方便在本机操作 Nginx 服务器,因此我在本机也创建了相同的目录,并使用 Portainer 将容器内相关的文件拷贝到本机。
ruby
docker container cp Nginx:/etc/nginx/nginx.conf /Users/xess/Code/LabProjects/chatgpt/dev-ops/nginx/conf
docker container cp Nginx:/etc/nginx/conf.d/default.conf /Users/xess/Code/LabProjects/chatgpt/dev-ops/nginx/conf.d/default.conf
docker container cp Nginx:/etc/nginx/conf.d/default.conf /Users/xess/Code/LabProjects/chatgpt/dev-ops/nginx/conf/conf.d
docker container cp Nginx:/usr/share/nginx/html/index.html /Users/xess/Code/LabProjects/chatgpt/dev-ops/nginx/html
然后将多个主机目录与文件挂载到容器内 Nginx 目录与文件,设置配置项,然后运行这个容器。
bash
docker run \
--name Nginx \
-p 80:80 \
-v /Users/xess/Code/LabProjects/chatgpt/dev-ops/nginx/logs:/var/log/nginx \
-v /Users/xess/Code/LabProjects/chatgpt/dev-ops/nginx/html:/usr/share/nginx/html \
-v /Users/xess/Code/LabProjects/chatgpt/dev-ops/nginx/conf/nginx.conf:/etc/nginx/nginx.conf \
-v /Users/xess/Code/LabProjects/chatgpt/dev-ops/nginx/conf/conf.d:/etc/nginx/conf.d \
-v /Users/xess/Code/LabProjects/chatgpt/dev-ops/nginx/ssl:/etc/nginx/ssl/ \
--privileged=true -d --restart always nginx
现在就可以在本机修改容器中的Nginx配置文件了。
接着我运行本机上的后端程序:
为什么要实现"Authentication Based on Subrequest Result"?后端程序自己鉴权不好吗?
基于我现有的理解水平,我总结了:
- 解耦了认证业务与逻辑业务,更符合软件开发中分层开发的设计理念,
- 对于多个服务或者分布式系统,这样适合集中管理和统一认证流程。以学校的 MIS 系统为例子,如果要访问其中选课系统、邮箱、缴费系统等多个服务,都可以通过配置 Nginx 实现统一的身份认证,认证通过后再访问具体的服务。
问题1. 后端程序认证服务没有收到请求
我在浏览器访问 localhost 可以成功显示 Nginx 默认页面,但是访问 localhost/api/?token=success 却显示 404,而且后端程序日志中也没有任何输出,说明 verify 控制器没有收到任何参数,究竟问题出在哪呢?
分析这个流程:
画了一个非常丑的图,将整个流程划分为 8 步:
- 第一步,客户端发送:http://localhost/api/?token=success。
- 第二步,Nginx 处理请求,并内部转发到 /auth 路径。
- 第三步,Nginx 处理 /auth 请求,取出 URL 参数赋值给
$query
,并构造新请求:[http://localhost:8080/verify?$query。](https://link.juejin.cn?target=http%3A%2F%2Flocalhost%3A8080%2Fverify%3F%24query%25E3%2580%2582 "http://localhost:8080/verify?$query。") - 第四步:后端程序处理此请求,并返回响应。
- 第五步:Nginx 根据后端的响应结果,决定是否继续处理主请求,这里默认返回 200 OK,继续处理。
- 第六步:Nginx 发送请求到:http://localhost:8080/success。
- 第七步:后端程序处理此请求,并返回响应。
- 第八步:Nginx 将响应返回给客户端。
因为问题主要是 verify 控制器没有收到任何参数,在确认了后端程序所监听的端口是正确的之后,那么问题就等价为:后端程序没有收到:http://localhost:8080/verify?token=success。 那么现在知道,问题只可能出现在第一步、第二步、第三步。
首先我通过本机上的 Postman,向 http://localhost:8080/verify?token=success 发送 GET 请求,发现成功响应,排除了第三步中接收端的可能性,剩下发送端。
通过修改 Nginx 配置文件中 location /api/ {} 以及对应 html 目录,发现 Nginx 服务器能将请求成功转发,因此排除了第一步的可能性。
现在只剩下第二步:Nginx内部转发请求以及第三步发送端:location=/auth{} 中的构造新请求并发出。
现在来重新检查第三步发送端:
bash
location = /auth { # 有等号表示精确匹配,没有表示前缀匹配
internal; # 表示这个位置是内部的,意味着这个路径不能直接通过客户端请求访问,只能通过 Nginx 的内部机制,如 auth_request 指令,进行访问。
# 设置参数
set $query '';
if ($request_uri ~* "[^?]+?(.*)$") {
set $query $1;
}
access_log /var/log/nginx/auth_access.log;
error_log /var/log/nginx/auth_error.log;
# 验证成功,返回200 OK
proxy_pass http://localhost:8080/verify?$query;
# 发送原始请求
proxy_pass_request_body off;
# 清空 Content-Type
proxy_set_header Content-Type "";
}
发现一个问题,它是向 localhost:8080 发送的一个 GET 请求,但是 Nginx 服务器是运行在 docker 容器中的,这里写 localhost:8080,实际上是 docker 的 localhost,而不是宿主机的 localhost 。因此得将 localhost 修改成宿主机也就是本机 IP 地址。修改之后,发现后端程序日志中有了输出,说明 verify 控制器成功接收到了请求,但是浏览器还是显示 404。
先来回顾一下,前一阶段的问题已经被成功解决,只需要修改 proxy_pass 中的主机地址就行,但是突然有一个问题,default.conf 中的 server_name 为什么可以写成 localhost 呢?这个 localhost 不应该也是容器内部而不是宿主机的吗?
这里先来明确一下 server_name 的意义,是明确来自哪的请求会被这个服务器块所处理。之所以在这里写 localhost 没事,是因为在启动 nginx 容器的时候把本机上所有可用的网络接口的 80 端口绑定到了 docker 容器的 80 端口,所以 docker 容器的 localhost 就等价于本机的 localhost,这里就可以写 localhost 而不用写本机 IP 地址。
问题2. Why running nginx shows port 0.0.0.0:80 instead of 127.0.0.1:80?
比如说办公室里有一台服务器,有多个网络接口:
- 有线网络接口(以太网):连接到办公室局域网,IP 地址为 192.168.1.10。
- Wi-Fi 网络接口:连接到办公室的无线网络,IP 地址为 192.168.2.10。
- 本地回环接口:localhost 或 127.0.0.1,这是计算机自身的接口,通常用于本地进程之间的通信。
如果这里写成 127.0.0.1:80->80,那么只能从本机访问 docker服务,如果写成 0.0.0.0:80->80,
这时,Nginx 可以通过以下方式访问:
- 通过有线网络:在办公室局域网的其他计算机上,输入 http://192.168.1.10 就可以访问 Nginx 服务。
- 通过无线网络:在办公室的 Wi-Fi 设备上,输入 http://192.168.2.10 也可以访问 Nginx 服务。
- 通过本地接口:在服务器本地,输入 http://localhost 或 http://127.0.0.1 可以访问 Nginx 服务。
问题3. ngx_http_auth_request 模型路径配置问题
现在回到出现的另一个问题,为什么还是显示 404?
因为 verify 能接收到请求并成功响应,说明问题出现在第五步、第六步、第七步、第八步。
其实写到这从直觉上有一个感悟,Nginx 内部这一套鉴权流程就像一个黑箱子,检查问题的话只能从外界与黑箱子之间的交互来,至于黑箱子内部的一些逻辑,很难去check,因此只能看文档。
找到了 Nginx 的官方文档:docs.nginx.com/nginx/admin...
这一部分叫:Authentication Based on Subrequest Result
NGINX and NGINX Plus can authenticate each request to your website with an external server or service. To perform authentication, NGINX makes an HTTP subrequest to an external server where the subrequest is verified. If the subrequest returns a
2xx
response code, the access is allowed, if it returns401
or403
, the access is denied. Such type of authentication allows implementing various authentication schemes, such as multifactor authentication, or allows implementing LDAP or OAuth authentication.
通过对照文档发现,在需要身份验证的地方,文档上写的是 location /private/ {},而我自己的配置文件上写的却是 location /private {}(这里实际上是 api,为了方便对比写成了 private)。我对照文档,多加了 '/',发现成功了。。。就是这么玄学。。
我接着去查找文档,为什么会这样呢?是基于某种通用的 Nginx 处理规则,还是这个 ngx_http_auth_request_module 模块对于语法规则的一些"小怪癖"。搜索无果,只能无奈在 stackoverflow 上发帖 stackoverflow.com/questions/7...,期待大神的帮助。
总结
写到这里,问题都顺利解决了,记录得也都差不多了,按照惯例来点总结。通过这次 debug 经历我还学到挺多的,对 Nginx 的理解与使用,对 docker 的理解(容器、镜像的概念、容器的运行、挂载),对于 HTTP 理解,对于计算机网络层面比如 0.0.0.0 与 127.0.0.1 的差别,最重要的是这种解决问题的思路,将一个大模块划分为一个小模块,然后使用排除法一个个排除,缩小范围,最终解决,以及在这个过程中积极地去 google(不是 chatgpt!)。因为我现在越来越发现,我的思维方式不是线形的、更不是树形的,它像一张网,随机游走甚至跳跃着遍历着节点,它倾向于使用直觉而不是逻辑分析,在定位第一个 bug 的时候,我将范围缩小到第二步与第三步发送端后,就开始失去目标,这时候是在不断尝试中打开了 access.log,在其中看到了一行"192.168.65.1",紧接着联想到昨天在评论区看到的一条相关建议,这才将问题定位到了第三步发送端,这个过程完全基于直觉,而非理性、严谨的排除与分析,这也是我下意识解决问题的一种方式,而这种思维方式在解决编程问题的过程中是非常低效甚至是让人感到痛苦的。回顾之前 debug 的经历,也许刚开始会进行分析,但当自己感到疲惫之后,大脑就会慢慢停转,于是就回归到这种基于直觉的方式,开始低效地一遍遍重复与依赖 AI 工具,这些都以不同程度与比重反复发生在我的学习过程中。因此要通过思考与这种思维惰性作斗争,也就是高中老师常说的"大脑要思考,不能停转",比如有意识地逼自己用线形或者树形的方式去思考,或者借助外界工具,使用思维导图、鱼骨图,或者在草稿纸上画出自己的思考过程等。