前言 

 

最近出现了一个这样的问题 

我启动了一个 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 端口不对, 导致访问不到 

01 容器端口映射导致 302 存在问题 以及 nginx 对于 302 的 Location 的重写_html

81 端口上面的 目标服务 

01 容器端口映射导致 302 存在问题 以及 nginx 对于 302 的 Location 的重写_nginx_02

读完此篇 希望你能够 明白问题之所在 

测试用例

"/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" 的响应结果

01 容器端口映射导致 302 存在问题 以及 nginx 对于 302 的 Location 的重写_java_03

后端服务 对于 Location 的处理

找到这个 相对来说比较简单  

如果 sendRediect 的 location 以 "//" 表示绝对路径, 直接拼接上 请求的协议返回 

否则 拼接上本域的相关信息 成为完整的 url 返回 

01 容器端口映射导致 302 存在问题 以及 nginx 对于 302 的 Location 的重写_java_04

然后 外层设置 status 为 302, 以及配置 Location 请求头给客户端 

01 容器端口映射导致 302 存在问题 以及 nginx 对于 302 的 Location 的重写_后端服务_05

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 涉及的相关代码 

01 容器端口映射导致 302 存在问题 以及 nginx 对于 302 的 Location 的重写_java_06

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 响应头 

01 容器端口映射导致 302 存在问题 以及 nginx 对于 302 的 Location 的重写_tomcat_07

nginx 拿到 Location 响应头之后重写为相对路径 

这个过程和 nginx 拿到请求之后 处理 发送给上游服务器是一个相反的过程 

这里是拿到  "http://localhost:8080/HelloWorld/listFormWithoutHeader" , 去掉 上游服务器的信息, 增加 "/api/" 前缀 

nginx 拿到请求之后 处理 发送给上游服务器是 增加 上游服务器的信息, 去掉 "/api/" 前缀 

01 容器端口映射导致 302 存在问题 以及 nginx 对于 302 的 Location 的重写_java_08

调试信息 

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"}