Ohhnews

分类导航

$ cd ..
Baeldung原文

Spring Boot中处理转发请求头的策略

#spring boot#转发头#反向代理#nginx#web开发

[LOADING...]

1. 概述

在现代 Web 服务中,我们会将后端服务与 API 网关、负载均衡器和反向代理等关键服务集成。因此,由于存在网络跳转,我们的后端服务不再了解客户端特定的网络细节,例如客户端 IP 地址、主机名和协议。

我们可以通过将请求详情作为 HTTP 转发头包含进来,克服这种可见性限制。

在本教程中,我们将学习在 Spring Boot 应用中处理转发头的各种策略。我们还将使用配置好的反向代理服务来实现并测试这些方法。

2. 什么是转发头?

转发头是指定网络相关元数据的 HTTP 头。上游服务在将请求处理到后端 API 时,会附加这些头。

传统上,许多 Web 服务器使用事实上的标准 X-Forwarded-* 头。

以下是 X-Forwarded-* 头的详细信息:

  • X-Forwarded-For — 指定原始客户端 IP 地址
  • X-Forwarded-By — 包含代理的 IP 地址以便追踪
  • X-Forwarded-Host — 包含客户端所提及的目标主机名
  • X-Forwarded-Port — 指定客户端用于连接的目标端口
  • X-Forwarded-Proto — 指定所使用的协议,即 http、https、ws 或 wss
  • X-Forwarded-Prefix — 指定任何路径前缀 URL,例如 /myapp,该前缀未包含在后端 URL 中

我们也可以使用 RFC 7239 标准中的 Forwarded 头:

$ bash
Forwarded: by=<identifier>; for=<identifier>; host=<host>; proto=<http/https>

使用转发头的目的可能各不相同,例如启用基于客户端 IP 的限流、审计、发送 HTTP URL(HATEOAS),或返回 HTTP 重定向响应。

3. 搭建示例应用

让我们实现一个带端点的后端服务。我们还将通过反向代理服务暴露该 API。

3.1. Maven 依赖

我们引入 spring-boot-starter 和 spring-boot-starter-web 依赖:

$ xml
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>4.1.1</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>

接下来,我们实现一个 REST API 服务。

3.2. 实现 API

我们将使用 RestController 类。首先,定义一个测试端点:

$ java
@GetMapping("/test")
public ResponseEntity<Void> test(HttpServletRequest request) {
    LOGGER.info("""
      scheme : {}
      Remote Host IP : {}
      Remote Port : {}
      Server Name : {}
      Server Port : {}
      Request URL : {}""", request.getScheme(), request.getRemoteHost(), request.getRemotePort(), request.getServerName(), request.getServerPort(),
        request.getRequestURL());
    return new ResponseEntity<>(HttpStatus.OK);
}

在这里,我们记录 HttpServletRequest 对象中存在的一些网络元数据。

3.3. 实现 Nginx 服务

现在,让我们配置一个 Nginx 服务,作为后端服务的反向代理。我们在 nginx.conf 文件中完成此配置:

$ config
worker_processes auto;
events {
    worker_connections 1024;
}
http {
    server {
        listen 443 ssl;
        server_name api.example.com;
        ssl_certificate     /etc/nginx/certs/api.example.com.crt;
        ssl_certificate_key /etc/nginx/certs/api.example.com.key;
        ssl_protocols TLSv1.2 TLSv1.3;
        location /myapp/ {
            proxy_pass http://backend-app:8081/;
            proxy_http_version 1.1;
            proxy_set_header Connection "";
        }
    }
}

我们设置了主机名 api.example.com、位置 /myapp/ 以及 TLS 证书。请注意,proxy_pass 中的末尾 / 是必需的,这样 Nginx 在转发请求时才能替换匹配的 /myapp/ 路径。

出于测试目的,我们使用 openssl 命令生成自签名证书:

$ bash
$ openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
  -keyout nginx/certs/api.example.com.key \
  -out nginx/certs/api.example.com.crt \
  -subj "/CN=api.example.com"

此外,我们需要将主机名 api.example.com 映射到 localhost 地址。

3.4. 实现 Docker 服务

让我们在 Docker 环境中实现整个设置。

首先,我们使用 Dockerfile 指定应用镜像:

$ bash
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY target/spring-boot-5-0.0.1-SNAPSHOT.jar backend-app.jar
EXPOSE 8081
ENTRYPOINT ["java", "-jar", "backend-app.jar"]

接下来,我们在 docker-compose.yml 文件中将上述服务组合在一起:

$ bash
services:
  backend-app:
    build:
      context: ../../../../../
      dockerfile: ./src/test/resources/docker/Dockerfile
    container_name: backend-app
    expose:
      - "8081"
  nginx:
    image: nginx:1.31.3-alpine
    container_name: nginx-proxy
    volumes:
      - ../../nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ../../nginx/certs:/etc/nginx/certs:ro
    ports:
      - "443:443"
    depends_on:
      - backend-app

在 docker-compose.yml 中,我们将后端端点配置为运行在 Docker 内部端口 8081 上。Nginx 服务可通过 443 端口上的 TLS 访问。

4. Spring Boot 中的不同策略

虽然上游服务应包含转发头,但我们仍需要在应用中处理这些头。

从 2.2.0 版本开始,Spring Boot 支持三种不同的策略。这些选项包括 None、Native 和 Framework 方法:

  • None 表示应用忽略转发头。
  • Native 让 Web 服务器处理转发头。
  • 使用 Framework 时,Spring 框架在服务请求之前处理这些转发头。

这通过 forward-headers-strategy 枚举属性进行配置。

默认策略是 None,除非 Spring Boot 检测到应用运行在已识别的云平台上,此时会启用 native 策略。

在 2.2.0 版本之前,我们可以选择忽略转发头,或者使用 server.use-forward-headers 布尔配置启用原生支持。## 5. 实现策略

让我们在 Spring Boot 的支持下实际看看这些转发头策略。

5.1. Nginx 中的转发头

在实现任何策略之前,我们需要在 Nginx 服务中包含转发头。

首先,在 Nginx 的 http 块中定义 forwarded_for 和 forwarded_by 变量:

$ bash
map $remote_addr $forwarded_for {
    ~: "\"[$remote_addr]:$remote_port\"";
    default "\"$remote_addr:$remote_port\"";
}
map $server_addr $forwarded_by {
    ~:  "\"[$server_addr]:$server_port\"";
    default "\"$server_addr:$server_port\"";
}

IP 地址和端口之间用冒号连接,从而同时支持 IPv4 和 IPv6 格式。

现在,在 nginx.conf 文件中使用 proxy_set_header 指令设置 X-Forwarded-*、Forwarded 和 Host 头:

$ config
location /myapp/ {
    proxy_pass http://backend-app:8081/;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Forwarded-For $remote_addr;
    proxy_set_header X-Forwarded-Host $host;
    proxy_set_header X-Forwarded-Port $server_port;
    proxy_set_header X-Forwarded-Prefix /myapp;
    
    proxy_set_header Forwarded "for=$forwarded_for;by=$forwarded_by;host=$host;proto=$scheme";
    proxy_http_version 1.1;
    proxy_set_header Connection "";
}

使用上述配置后,Nginx 服务会自动包含转发头。

与其覆盖 X-Forwarded-For 的值,我们可以追加代理的 IP,以保留可信上游服务的 X-Forwarded-For 头。我们可以使用内置的 proxy_add_x_forwarded_for 变量来追加该头:

$ bash
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

仅当 XFF 头来自可信的内部服务时才应追加它。这将确保客户端无法伪造转发头。

5.2. None 策略

假设我们希望忽略转发头,并且无法信任它们。例如,应用程序可能直接暴露给客户端,或者代理不处理转发头。在这种情况下,我们使用 None 策略。

首先,在 application.yml 文件中将 forward-headers-strategy 配置为 NONE:

$ config
server:
  port: 8081
  forward-headers-strategy: NONE

为了测试,我们在启用 Nginx 反向代理的情况下运行上述配置。然后,使用 docker compose 命令运行上述 Docker 服务:

$ bash
$ docker compose up --build

最后,在终端中执行 /myapp/test 端点:

$ bash
$ curl -k https://api.example.com/myapp/test

我们可以从 Nginx 日志中确认请求详情:

$ bash
192.168.65.1 - - [26/Aug/2026:08:13:08 +0000] "GET /myapp/test HTTP/1.1" 200 0 "-" "curl/8.7.1"

此外,我们需要在应用程序日志中验证 HttpServletRequest 属性:

$ bash
2026-08-26T08:13:08.124Z  INFO 1 --- [backend-app] [nio-8081-exec-1] c.b.forwardheaders.TestController:
scheme : http
Remote Host IP : 172.21.0.3
Remote Port : 50754
Server Name : api.example.com
Server Port : 80
Request URL : http://api.example.com/test

日志确认,scheme、远程 IP、服务器端口和请求 URL 与 Nginx 服务器接收到的内容不同。

此外,让我们确认 Nginx 服务的 IP 地址:

$ bash
$ docker exec -it nginx-proxy /bin/sh 
/ # ifconfig
eth0      Link encap:Ethernet  HWaddr 2A:4C:7C:42:E0:87  
          inet addr:172.21.0.3  Bcast:172.21.255.255  Mask:255.255.0.0

客户端 IP 地址是 172.21.0.3,即 Nginx 服务的 IP 地址。然而,在 Nginx 日志中,我们看到客户端 IP 地址是 192.168.65.1,这实际上是 Docker Desktop 的默认网关地址。正如预期,后端服务没有处理转发头。

5.3. Native 策略

使用 Native 策略时,Spring Boot 将转发头的处理委托给底层 Web 服务器。其用例可以是审计、启用基于 IP 的限流,或提供可访问的重定向 URL。

首先,将 forward-headers-strategy 配置为 NATIVE,并加入与 Tomcat 相关的 remoteip 配置:

$ config
server:
  port: 8081
  forward-headers-strategy: NATIVE
  tomcat:
    remoteip:
      remote-ip-header: X-Forwarded-For
      protocol-header: X-Forwarded-Proto
      host-header: X-Forwarded-Host
      port-header: X-Forwarded-Port
      trusted-proxies: 172\.(1[6-9]|2\d|3[0-1])\.\d{1,3}\.\d{1,3}

在上述配置中,我们同时设置了 X-Forwarded-* 名称和 trusted-proxies IP 范围模式。remoteip 配置是可选的,因为 Tomcat 服务器实现了合理的默认值。

然后,在服务器上启用访问日志:

$ config
server:
  tomcat:
    accesslog:
      enabled: true
      request-attributes-enabled: true
      directory: /app/logs
      prefix: access_log
      suffix: .log
      pattern: '%h %l %u %t "%r" %s %b "%{X-Forwarded-Host}i" "%{X-Forwarded-For}i" "%{X-Forwarded-Proto}i"'
      rotate: false

在内部,Tomcat 服务器实现了一个名为 RemoteIpValve 的阀,用于处理配置的转发头。

最后,我们执行相同的测试端点以验证应用程序日志:

$ bash
2026-08-26T08:15:31.672Z  INFO 1 --- [backend-app] [nio-8081-exec-1] c.b.forwardheaders.TestController:
scheme : https
Remote Host IP : 192.168.65.1
Remote Port : 56444
Server Name : api.example.com
Server Port : 443
Request URL : https://api.example.com/test

日志确认了预期的 scheme、远程 IP、服务器名称和服务器端口。

然而,请求 URL 没有包含 /myapp 前缀。原因是 RemoteIpValve 实现忽略了 X-Forwarded-Prefix 头。

此外,我们在 Docker 环境内的服务器访问日志中验证转发头:

$ bash
$ docker exec -it backend-app /bin/sh
/app # tail -f ./logs/access_log.log
192.168.65.1 - - [26/Aug/2026:08:15:31 +0000] "GET /test HTTP/1.1" 200 - "api.example.com" "192.168.65.1" "https"

除了请求 URL 缺少前缀外,Native 方案按预期工作。

客户端请求的整体流程如下:

[LOADING...]

尽管我们正确处理了转发头,但仍需要在请求 URL 中包含前缀。

5.4. Framework 策略

在这种方法中,Spring Boot 在内部处理所有转发头,包括 X-Forwarded-Prefix 头。这将使我们能够在 URL 中包含前缀。

在 application.yml 文件中将 forward-headers-strategy 启用为 FRAMEWORK:

$ config
server:
  port: 8081
  forward-headers-strategy: FRAMEWORK

在内部,Spring Boot 使用 ForwardedHeaderFilter 类实现此策略。

让我们执行 /myapp/test 端点并验证应用程序日志:

$ bash
2026-08-26T08:18:07.197Z  INFO 1 --- [backend-app] [nio-8081-exec-1] c.b.forwardheaders.TestController:
scheme : https
Remote Host IP : 192.168.65.1
Remote Port : 52414
Server Name : api.example.com
Server Port : 443
Request URL : https://api.example.com/myapp/test

我们确认了 scheme、远程 IP/端口、服务器名称/端口和请求 URL 均符合预期。此外,ForwardedHeaderFilter 会根据配置的 Forwarded 头正确提取远程 IP 和端口。默认情况下,如果存在 Forwarded: for 头,则使用它更新远程 IP 和端口。否则,仅使用 X-Forwarded-For 头更新远程 IP。

上述行为是在 Spring Boot 2.4.0 中实现的。在 Spring Boot 2.4.0/Spring Framework 5.3.0 之前,远程 IP 和端口保持不变。

我们不应同时启用 Framework 和 Native 策略,因为这会导致重复处理。

6. 结论

在本文中,我们了解了转发头以及为什么需要它们。

我们还演示并测试了 Spring Boot 提供的三种策略:通过 forward-headers-strategy 配置的 None、Native 和 Framework 策略。

一如既往,文章代码可在 GitHub 上找到。