• 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 映射端口, 宿主机的端口 和 容器中的端口不一致 

    1. master:nginx jerry$ cat docker-compose.yml
    2. version: "2"
    3. services:
    4. nginx:
    5. container_name: nginx
    6. image: nginx:latest
    7. ports:
    8. - "81:80"
    9. volumes:
    10. - ./data:/etc/nginx
    11. # - ./html:/usr/share/nginx/html
    12. - /Users/jerry/WebstormProjects/HelloWorld:/usr/share/nginx/html

    nginx 到 后端服务 的路由配置 

    1. location ^~ /api/ {
    2. root html;
    3. index index.html index.htm;
    4. proxy_pass http://192.168.0.104:8080/;
    5. }

    访问这个 sendRedirect, 结果发现 Location 之后的 url 端口不对, 导致访问不到 

    81 端口上面的 目标服务 

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

    测试用例

    "/HelloWorld/sendRedirect" 以及 "/HelloWorld/listFormWithoutHeader" 的相关业务代码 

    1. /**
    2. * HelloWorldController
    3. *
    4. * @author Jerry.X.He <970655147@qq.com>
    5. * @version 1.0
    6. * @date 2022-02-27 21:21
    7. */
    8. @RestController
    9. @RequestMapping("/HelloWorld")
    10. public class HelloWorldController {
    11. @RequestMapping("/sendRedirect")
    12. public void sendRedirect(HttpServletResponse response) throws Exception {
    13. response.sendRedirect("/HelloWorld/listFormWithoutHeader");
    14. }
    15. @GetMapping("/listFormWithoutHeader")
    16. public List<JSONObject> listFormWithoutHeader(
    17. String param01, String param02
    18. ) {
    19. List<JSONObject> result = new ArrayList<>();
    20. result.add(wrapEntity("param01", param01));
    21. result.add(wrapEntity("param02", param02));
    22. userService.list();
    23. return result;
    24. }
    25. }

    nginx 里面 location 的配置

    1. location ^~ /api/ {
    2. root html;
    3. index index.html index.htm;
    4. proxy_pass http://localhost:8080/;
    5. }

    浏览器中访问 "/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 应该需要拼接上 当前域 的相关信息 

    1. (gdb) b ngx_http_upstream.c:2432
    2. Note: breakpoint 1 also set at pc 0x10d5bedda.
    3. Breakpoint 2 at 0x10d5bedda: file src/http/ngx_http_upstream.c, line 2432.
    4. (gdb) c
    5. Continuing.
    6. Breakpoint 1, ngx_http_upstream_process_header (r=0x7f8f0100b850,
    7. u=0x7f8f0100cbe0) at src/http/ngx_http_upstream.c:2432
    8. 2432 if (ngx_http_upstream_process_headers(r, u) != NGX_OK) {
    9. (gdb) print r->headers_out.location
    10. $1 = (ngx_table_elt_t *) 0x0
    11. (gdb) next
    12. 2436 ngx_http_upstream_send_response(r, u);
    13. (gdb) print r->headers_out.location
    14. $2 = (ngx_table_elt_t *) 0x7f8f0100be00
    15. (gdb) print r->headers_out.location.value
    16. $3 = {len = 37, data = 0x7f8f0100d659 "/api/HelloWorld/listFormWithoutHeader"}

    nginx 拼接上当前域的相关信息到 Location 涉及的相关代码 

    nginx 拼接上当前域的相关信息到 Location 

    1. Breakpoint 3, ngx_http_header_filter (r=0x7f8f01826450)
    2. at src/http/ngx_http_header_filter_module.c:321
    3. 321 c = r->connection;
    4. (gdb) next
    5. 323 if (r->headers_out.location
    6. (gdb) next
    7. 324 && r->headers_out.location->value.len
    8. (gdb) next
    9. 325 && r->headers_out.location->value.data[0] == '/'
    10. (gdb) next
    11. 326 && clcf->absolute_redirect)
    12. (gdb) next
    13. 323 if (r->headers_out.location
    14. (gdb) next
    15. 328 r->headers_out.location->hash = 0;
    16. (gdb) next
    17. 330 if (clcf->server_name_in_redirect) {
    18. (gdb) next
    19. 335 host = r->headers_in.server;
    20. (gdb) next
    21. 337 } else {
    22. (gdb) next
    23. 346 port = ngx_inet_get_port(c->local_sockaddr);
    24. (gdb) next
    25. 349 + host.len
    26. (gdb) next
    27. 350 + r->headers_out.location->value.len + 2;
    28. (gdb) next
    29. 348 len += sizeof("Location: https://") - 1
    30. (gdb) next
    31. 352 if (clcf->port_in_redirect) {
    32. (gdb) print host
    33. $4 = {len = 9, data = 0x7f8f01800031 "localhost"}
    34. (gdb) print port
    35. $5 = 80
    36. (gdb) b ngx_http_header_filter_module.c:517
    37. Breakpoint 4 at 0x10d5ce56c: file src/http/ngx_http_header_filter_module.c, line 517.
    38. (gdb) c
    39. Continuing.
    40. Breakpoint 4, ngx_http_header_filter (r=0x7f8f01826450)
    41. at src/http/ngx_http_header_filter_module.c:517
    42. 517 if (host.data) {
    43. (gdb) print host
    44. $6 = {len = 9, data = 0x7f8f01800031 "localhost"}
    45. (gdb) next
    46. 519 p = b->last + sizeof("Location: ") - 1;
    47. (gdb) next
    48. 521 b->last = ngx_cpymem(b->last, "Location: http",
    49. (gdb) next
    50. 530 *b->last++ = ':'; *b->last++ = '/'; *b->last++ = '/';
    51. (gdb) print b->start
    52. $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"
    53. (gdb) next
    54. 531 b->last = ngx_copy(b->last, host.data, host.len);
    55. (gdb) next
    56. 533 if (port) {
    57. (gdb) next
    58. 537 b->last = ngx_copy(b->last, r->headers_out.location->value.data,
    59. (gdb) next
    60. 542 r->headers_out.location->value.len = b->last - p;
    61. (gdb) print b->start
    62. $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"
    63. (gdb) next
    64. 543 r->headers_out.location->value.data = p;
    65. (gdb) next
    66. 544 ngx_str_set(&r->headers_out.location->key, "Location");
    67. (gdb) next
    68. 546 *b->last++ = CR; *b->last++ = LF;
    69. (gdb) next
    70. 549 if (r->chunked) {
    71. (gdb) print r->headers_out.location.value
    72. $9 = {len = 53,
    73. data = 0x7f8f02804e87 "http://localhost/api/HelloWorld/listFormWithoutHeader\r\n"}

    客户端拿到的 Location 响应头 

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

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

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

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

    调试信息 

    1. Breakpoint 5, ngx_http_proxy_rewrite (r=0x7f8f04000450, h=0x7f8f04000a00,
    2. prefix=0, len=22, replacement=0x7ffee26c3cf0)
    3. at src/http/modules/ngx_http_proxy_module.c:2713
    4. 2713 new_len = replacement->len + h->value.len - len;
    5. (gdb) print h->value
    6. $17 = {len = 54,
    7. data = 0x7f8f04002259 "http://localhost:8080/HelloWorld/listFormWithoutHeader"}
    8. (gdb) print replacement
    9. $18 = (ngx_str_t *) 0x7ffee26c3cf0
    10. (gdb) print replacement.data
    11. $19 = (u_char *) 0x7f8f0181e9dc "/api/"
    12. (gdb) info locals
    13. p = 0x7fff718efafc <small_malloc_should_clear+284> "L\211\347I\211\304H\205\300\017\205%\004"
    14. data = 0x7ffee26c3d50 "\003"
    15. new_len = 8
    16. (gdb) print len
    17. $20 = 22
    18. (gdb) next
    19. 2715 if (replacement->len > len) {
    20. (gdb) next
    21. 2731 p = ngx_copy(h->value.data + prefix, replacement->data,
    22. (gdb) next
    23. 2734 ngx_memmove(p, h->value.data + prefix + len,
    24. (gdb) next
    25. 2738 h->value.len = new_len;
    26. (gdb) next
    27. 2740 return NGX_OK;
    28. (gdb) print h->value
    29. $21 = {len = 37, data = 0x7f8f04002259 "/api/HelloWorld/listFormWithoutHeader"}

  • 相关阅读:
    Vue学习第17天——netTick()的原理及使用
    G1D22-安装burpsuite&AttacKG
    【博士每天一篇文献-算法】Gradient Episodic Memory for Continual Learning
    通讯网关软件019——利用CommGate X2OPCUA实现OPC UA访问Oracle服务器
    2022牛客多校联赛第四场 题解
    SentinelResource注解详解
    Java中List不同实现类的对比
    postgresql 格式化查询树为图片 —— pgNodeGraph 与 pg_node2graph
    linux软件安装
    智能电网短路故障接地故障模拟柜
  • 原文地址:https://blog.csdn.net/u011039332/article/details/125466771