SpringBoot 中对 Forwarded 请求头的处理

SpringBoot 中对 Forwarded 请求头的处理

然后是
SpringBoot 内嵌 Tomcat 的时候,可以通过 forward-headers-strategy 来判断 forward 请求头的处理方式,不同的配置有不同的结果,这个不同指的是什么
https://stackoverflow.com/questions/68318269/spring-server-forward-headers-strategy-native-vs-framework

在 ServletWebServerFactoryAutoConfiguration 自动配置类中,forwardedHeaderFilter 方法中我们可以看到,如果 forward-headers-strategy 配置为 FRAMEWORK,则会创建过滤器 ForwardedHeaderFilter,这个过滤器的作用是什么呢?

起作用很简单,就是从 ForwardedX-Forwarded-* 报头中提取值,然后包装请求和响应,并在以下方法中起作用,使它们返回的信息,是客户端发起的那个请求和响应协议和目标 IP,也就是帮助我们剔除代理的影响,让我们可以正常获取发起请求的原始客户端的信息
实现方式,就是自定义了 HttpServletRequest 的实现为 ForwardedHeaderExtractingRequest
自定义了 HttpServletResponse 的实现为 RelativeRedirectResponseWrapper 或者 ForwardedHeaderExtractingResponse
做的事情很简单

HTTP headers 官方文档,以及 X-Forwarded-For 消息头

核心起作用的方法是 doFilterInternal

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

if (this.removeOnly) {
ForwardedHeaderRemovingRequest wrappedRequest = new ForwardedHeaderRemovingRequest(request);
filterChain.doFilter(wrappedRequest, response);
}
else {
HttpServletRequest wrappedRequest =
new ForwardedHeaderExtractingRequest(request);

HttpServletResponse wrappedResponse = this.relativeRedirects ?
RelativeRedirectResponseWrapper.wrapIfNecessary(response, HttpStatus.SEE_OTHER) :
new ForwardedHeaderExtractingResponse(response, wrappedRequest);

filterChain.doFilter(wrappedRequest, wrappedResponse);
}
}

写个例子,用 nginx 转发试试,TODO

简单实践

刚搭建完的时候,nginx 没有添加 forward 相关请求头,所以 shouldNotFilter 方法返回 true,也就是不进行过滤,
此时,请求的来源地址,是 nginx 发起的请求,并不是客户端发起的请求,客户端发起的请求是 http://localhost:8081/proxy

{
"scheme": "http",
"requestURL": "http://localhost:8080/SpringBoot-ForwardHeader/target",
"contextPath": "/SpringBoot-ForwardHeader",
"serverName": "localhost",
"requestURI": "/SpringBoot-ForwardHeader/target",
"serverPort": 8080
}

下面我们来添加相关请求头,

http {

   ...

    server {
        listen       8081;
        server_name  localhost;
        location /proxy {
            proxy_pass http://localhost:8080/SpringBoot-ForwardHeader/target;
            proxy_set_header            Host $host;
            proxy_set_header            X-Forwarded-Host $remote_addr;
            proxy_set_header            X-Forwarded-Port $remote_port;
            proxy_set_header            X-Forwarded-Proto $scheme;
            proxy_set_header            X-Forwarded-Prefix $uri ;
        }
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }
    }

}

配置我们的 web 应用

server:
  application:
    name: ForwardHeader
  # 当前应用所在的 Web 服务器监听的本地端口
  port: 8080
  servlet:
    # 应用的上下文路径
    context-path: /SpringBoot-ForwardHeader
    # 启用默认的 Servlet
    register-default-servlet: true
  forward-headers-strategy: framework

此时,prefix 可以正常显示,

{
  "scheme": "http",
  "requestURL": "http://127.0.0.1:62031/proxy/target",
  "contextPath": "/proxy",
  "serverName": "127.0.0.1",
  "requestURI": "/proxy/target",
  "serverPort": 62031
}

手机 IP 为 192.168.43.68

{
  "scheme": "http",
  "requestURL": "http://192.168.43.68:48476/proxy/target",
  "contextPath": "/proxy",
  "serverName": "192.168.43.68",
  "requestURI": "/proxy/target",
  "serverPort": 48476
}

但是换成 native 之后,prefix 无法正常显示

server:
  application:
    name: ForwardHeader
  # 当前应用所在的 Web 服务器监听的本地端口
  port: 8080
  servlet:
    # 应用的上下文路径
    context-path: /SpringBoot-ForwardHeader
    # 启用默认的 Servlet
    register-default-servlet: true
  forward-headers-strategy: native
{
  "scheme": "http",
  "requestURL": "http://127.0.0.1:61876/SpringBoot-ForwardHeader/target",
  "contextPath": "/SpringBoot-ForwardHeader",
  "serverName": "127.0.0.1",
  "requestURI": "/SpringBoot-ForwardHeader/target",
  "serverPort": 61876
}

手机

{
  "scheme": "http",
  "requestURL": "http://192.168.43.68:48092/SpringBoot-ForwardHeader/target",
  "contextPath": "/SpringBoot-ForwardHeader",
  "serverName": "192.168.43.68",
  "requestURI": "/SpringBoot-ForwardHeader/target",
  "serverPort": 48092
}

none

{
  "scheme": "http",
  "requestURL": "http://192.168.43.42/SpringBoot-ForwardHeader/target",
  "contextPath": "/SpringBoot-ForwardHeader",
  "serverName": "192.168.43.42",
  "requestURI": "/SpringBoot-ForwardHeader/target",
  "serverPort": 80
}

可以看到没有一点转发头的影子,


注意在这个过滤器中,会清除转发(forward)相关的请求头,

forward-headers-strategy 默认是 none,不对 forward 请求头进行处理,TODO,

对比一下这两者的区别,native 和 none 的区别


研究结束,TODO,用手机看更明显,因此,我们再次回到我们的原点,这个配置是干什么的,这个配置是帮助我们剔除代理的影响,让我们可以正常获取发起请求的原始客户端的信息。


这个配置项,过滤器,本质上就是在捕捉 http 消息头,

所以只要一个工具可以添加相关请求头,就可以模拟 http 转发时 springboot 的处理。并不一定非得用 NGINX,用 postman 构造相关请求头也是可以的。