01 容器端口映射导致 302 存在问题 以及 nginx 对于 302 的 Location 的重写
Posted 蓝风9
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了01 容器端口映射导致 302 存在问题 以及 nginx 对于 302 的 Location 的重写相关的知识,希望对你有一定的参考价值。
前言
最近出现了一个这样的问题
我启动了一个 nginx 的一个 docker 容器
然后 容器的端口为 80, 映射到宿主机 81 端口
后端服务有一个 sendRedirect("/xxx"), 然后 客户端这边 拿到的端口是 80
然后 导致客户端 访问不到, 本文的一些 知识是 衍生自这个问题
302 转发相关的流程
从前端页面 到 nginx, 转发了一个请求 "/api/HelloWorld/sendRedirect"
nginx 将请求转发给了 后台服务, "/HelloWorld/sendRedirect"
然后 这个后台服务 中有一个 sendRedirect("/HelloWorld/listFormWithoutHeader")
然后 由后台服务 将响应回复给 nginx "http://localhost:8080/HelloWorld/listFormWithoutHeader"
然后 nginx 将相应 回复给客户端 "http://localhost/api/HelloWorld/listFormWithoutHeader"
可以看到 这个过程中 location 这个请求头是有数次调整
第一层是 后端服务的处理, 发现 sendRediect 的请求不是绝对地址, 自动拼接上了 本域 的相关信息
第二层是 nginx 的处理, 去掉了 后端服务的域的相关信息, 拼接上了 本域的相关信息
我们这里 就来梳理一下 这里的整个流程
以下截图, 调试基于 nginx-1.18.0
端口映射导致 Location 访问不到原始问题现象
docker-compose 映射端口, 宿主机的端口 和 容器中的端口不一致
master:nginx jerry$ cat docker-compose.yml
version: "2"
services:
nginx:
container_name: nginx
image: nginx:latest
ports:
- "81:80"
volumes:
- ./data:/etc/nginx
# - ./html:/usr/share/nginx/html
- /Users/jerry/WebstormProjects/HelloWorld:/usr/share/nginx/html
nginx 到 后端服务 的路由配置
location ^~ /api/
root html;
index index.html index.htm;
proxy_pass http://192.168.0.104:8080/;
访问这个 sendRedirect, 结果发现 Location 之后的 url 端口不对, 导致访问不到
81 端口上面的 目标服务
读完此篇 希望你能够 明白问题之所在
测试用例
"/HelloWorld/sendRedirect" 以及 "/HelloWorld/listFormWithoutHeader" 的相关业务代码
/**
* HelloWorldController
*
* @author Jerry.X.He <970655147@qq.com>
* @version 1.0
* @date 2022-02-27 21:21
*/
@RestController
@RequestMapping("/HelloWorld")
public class HelloWorldController
@RequestMapping("/sendRedirect")
public void sendRedirect(HttpServletResponse response) throws Exception
response.sendRedirect("/HelloWorld/listFormWithoutHeader");
@GetMapping("/listFormWithoutHeader")
public List<JSONObject> listFormWithoutHeader(
String param01, String param02
)
List<JSONObject> result = new ArrayList<>();
result.add(wrapEntity("param01", param01));
result.add(wrapEntity("param02", param02));
userService.list();
return result;
nginx 里面 location 的配置
location ^~ /api/
root html;
index index.html index.htm;
proxy_pass http://localhost:8080/;
浏览器中访问 "/HelloWorld/sendRedirect" 结果如下
可以看到 "/HelloWorld/sendRedirect" 的响应是 302, 跳转到了这里的 "http://localhost/api/HelloWorld/listFormWithoutHeader"
页面上显示的 即为 "http://localhost/api/HelloWorld/listFormWithoutHeader" 的响应结果
后端服务 对于 Location 的处理
找到这个 相对来说比较简单
如果 sendRediect 的 location 以 "//" 表示绝对路径, 直接拼接上 请求的协议返回
否则 拼接上本域的相关信息 成为完整的 url 返回
然后 外层设置 status 为 302, 以及配置 Location 请求头给客户端
nginx 对于 Location 的处理
我们先看一下 nginx 拿到 location 请求头之后的处理
可以看到的是 服务端响应的 "Location" 是 "http://localhost:8080/HelloWorld/listFormWithoutHeader"
但是 这里 ngx_http_upstream_process_headers 处理之后, 去掉了 上游服务器的 域的相关信息, 拼接上了 "/api/"前缀 和 "/HelloWorld/listFormWithoutHeader"
注意 这里的 locatoin 又成为了一个 相对路径, 所以 参照上面 tomcat 的做法, nginx 应该需要拼接上 当前域 的相关信息
(gdb) b ngx_http_upstream.c:2432
Note: breakpoint 1 also set at pc 0x10d5bedda.
Breakpoint 2 at 0x10d5bedda: file src/http/ngx_http_upstream.c, line 2432.
(gdb) c
Continuing.
Breakpoint 1, ngx_http_upstream_process_header (r=0x7f8f0100b850,
u=0x7f8f0100cbe0) at src/http/ngx_http_upstream.c:2432
2432 if (ngx_http_upstream_process_headers(r, u) != NGX_OK)
(gdb) print r->headers_out.location
$1 = (ngx_table_elt_t *) 0x0
(gdb) next
2436 ngx_http_upstream_send_response(r, u);
(gdb) print r->headers_out.location
$2 = (ngx_table_elt_t *) 0x7f8f0100be00
(gdb) print r->headers_out.location.value
$3 = len = 37, data = 0x7f8f0100d659 "/api/HelloWorld/listFormWithoutHeader"
nginx 拼接上当前域的相关信息到 Location 涉及的相关代码
nginx 拼接上当前域的相关信息到 Location
Breakpoint 3, ngx_http_header_filter (r=0x7f8f01826450)
at src/http/ngx_http_header_filter_module.c:321
321 c = r->connection;
(gdb) next
323 if (r->headers_out.location
(gdb) next
324 && r->headers_out.location->value.len
(gdb) next
325 && r->headers_out.location->value.data[0] == '/'
(gdb) next
326 && clcf->absolute_redirect)
(gdb) next
323 if (r->headers_out.location
(gdb) next
328 r->headers_out.location->hash = 0;
(gdb) next
330 if (clcf->server_name_in_redirect)
(gdb) next
335 host = r->headers_in.server;
(gdb) next
337 else
(gdb) next
346 port = ngx_inet_get_port(c->local_sockaddr);
(gdb) next
349 + host.len
(gdb) next
350 + r->headers_out.location->value.len + 2;
(gdb) next
348 len += sizeof("Location: https://") - 1
(gdb) next
352 if (clcf->port_in_redirect)
(gdb) print host
$4 = len = 9, data = 0x7f8f01800031 "localhost"
(gdb) print port
$5 = 80
(gdb) b ngx_http_header_filter_module.c:517
Breakpoint 4 at 0x10d5ce56c: file src/http/ngx_http_header_filter_module.c, line 517.
(gdb) c
Continuing.
Breakpoint 4, ngx_http_header_filter (r=0x7f8f01826450)
at src/http/ngx_http_header_filter_module.c:517
517 if (host.data)
(gdb) print host
$6 = len = 9, data = 0x7f8f01800031 "localhost"
(gdb) next
519 p = b->last + sizeof("Location: ") - 1;
(gdb) next
521 b->last = ngx_cpymem(b->last, "Location: http",
(gdb) next
530 *b->last++ = ':'; *b->last++ = '/'; *b->last++ = '/';
(gdb) print b->start
$7 = (u_char *) 0x7f8f02804e20 "HTTP/1.1 302 \\r\\nServer: nginx/1.18.0\\r\\nDate: Sun, 26 Jun 2022 01:40:21 GMT\\r\\nContent-Length: 0\\r\\nLocation: http"
(gdb) next
531 b->last = ngx_copy(b->last, host.data, host.len);
(gdb) next
533 if (port)
(gdb) next
537 b->last = ngx_copy(b->last, r->headers_out.location->value.data,
(gdb) next
542 r->headers_out.location->value.len = b->last - p;
(gdb) print b->start
$8 = (u_char *) 0x7f8f02804e20 "HTTP/1.1 302 \\r\\nServer: nginx/1.18.0\\r\\nDate: Sun, 26 Jun 2022 01:40:21 GMT\\r\\nContent-Length: 0\\r\\nLocation: http://localhost/api/HelloWorld/listFormWithoutHeader"
(gdb) next
543 r->headers_out.location->value.data = p;
(gdb) next
544 ngx_str_set(&r->headers_out.location->key, "Location");
(gdb) next
546 *b->last++ = CR; *b->last++ = LF;
(gdb) next
549 if (r->chunked)
(gdb) print r->headers_out.location.value
$9 = len = 53,
data = 0x7f8f02804e87 "http://localhost/api/HelloWorld/listFormWithoutHeader\\r\\n"
客户端拿到的 Location 响应头
nginx 拿到 Location 响应头之后重写为相对路径
这个过程和 nginx 拿到请求之后 处理 发送给上游服务器是一个相反的过程
这里是拿到 "http://localhost:8080/HelloWorld/listFormWithoutHeader" , 去掉 上游服务器的信息, 增加 "/api/" 前缀
nginx 拿到请求之后 处理 发送给上游服务器是 增加 上游服务器的信息, 去掉 "/api/" 前缀
调试信息
Breakpoint 5, ngx_http_proxy_rewrite (r=0x7f8f04000450, h=0x7f8f04000a00,
prefix=0, len=22, replacement=0x7ffee26c3cf0)
at src/http/modules/ngx_http_proxy_module.c:2713
2713 new_len = replacement->len + h->value.len - len;
(gdb) print h->value
$17 = len = 54,
data = 0x7f8f04002259 "http://localhost:8080/HelloWorld/listFormWithoutHeader"
(gdb) print replacement
$18 = (ngx_str_t *) 0x7ffee26c3cf0
(gdb) print replacement.data
$19 = (u_char *) 0x7f8f0181e9dc "/api/"
(gdb) info locals
p = 0x7fff718efafc <small_malloc_should_clear+284> "L\\211\\347I\\211\\304H\\205\\300\\017\\205%\\004"
data = 0x7ffee26c3d50 "\\003"
new_len = 8
(gdb) print len
$20 = 22
(gdb) next
2715 if (replacement->len > len)
(gdb) next
2731 p = ngx_copy(h->value.data + prefix, replacement->data,
(gdb) next
2734 ngx_memmove(p, h->value.data + prefix + len,
(gdb) next
2738 h->value.len = new_len;
(gdb) next
2740 return NGX_OK;
(gdb) print h->value
$21 = len = 37, data = 0x7f8f04002259 "/api/HelloWorld/listFormWithoutHeader"
完
以上是关于01 容器端口映射导致 302 存在问题 以及 nginx 对于 302 的 Location 的重写的主要内容,如果未能解决你的问题,请参考以下文章
01 容器端口映射导致 302 存在问题 以及 nginx 对于 302 的 Location 的重写
06 nginx 处理转发其他域的处理 以及 proxy_redirect
06 nginx 处理转发其他域的处理 以及 proxy_redirect