转载

使用X-Forwarded-*正确处理浏览器端地址信息

在WEB开发中,有一定的场景中需要拿到基于浏览器视角的路径信息。比如在spring hateoas, spring security,以及基于302跳转的各类登陆,认证体系。在这种场景中,服务端拿到的地址信息可能并不是实际浏览器端访问的路径信息,这时候就要考虑地址转换的事务。特别是当前的业务系统必定前端有Nginx,以及https转http类的反向代理等场景。

本文描述了标准的 X-Forwarded-Host, X-Forwarded-Port 和 X-Forwarded-Proto的使用以及在部分框架中使用的 X-Forwarded-Prefix。同时,在业务框架(如Spring)中已经实际应用的示例代码。在具体场景下应当如何来进行正确地使用。

这些头信息Nginx以及浏览器不会自动地添加到请求头中,需要显示地进行配置。

X-Forwarded-Host

一般配置为 $http_host, 表示获取浏览器在请求时的 HOST 头

X-Forwarded-Port

表示浏览器在访问时的实际端口时,如nginx提供http服务,则为80,如果是https,则为 443。如果为标准服务,此项可以不用设置,仅用于非标准端口时才使用

X-Forwarded-Proto

表示浏览器在访问时的实际协议,如nginx提供https转http反向代理时,这里即要配置为 https,表示浏览器是使用https协议来进行访问,但后端可能用http提供服务。如果两边协议相同,则不用设置

X-Forwarded-Prefix

用于当nginx使用前缀代理后端请求时使用。如 浏览器请求的路径为 serviceName/a/b/c, Nginx在Location端通过serviceName匹配到后端服务时,但把前缀 serviceName去掉,实际访问后端为 /a/b/c时,这时即需要设置此值。

实际示例

浏览器访问地址为

https://www.iflym.com/y2b/userinfo/me

https协议由nginx负责处理. 后端为java springboot 服务,contextPath为/,实际处理地址为 /userinfo/me,ip为 1.2.3.4,端口为 8080. 那么在nginx中的详细配置如下

# server层已配置好https,下面不再处理
Location ~* ^/y2b(.+) {
    proxy_set_header Host $proxy_host;

    proxy_set_header X-Forwarded-Host $http_host;
    proxy_set_header X-Forwarded-Proto https;
    proxy_set_header X-Forwarded-Port 443; #标准端口,可不配置
    proxy_set_header X-Forwarded-Prefix /y2b;

	proxy_pass http://1.2.3.4:8080;
}

后端应用

在实际的业务代码中,理论上应当很少去访问如 request.getSchema, request.getServerName(), request.getContextPath, request.getRequestURI 这些API. 这些API均从标准的http访问头上进行获取,即从nginx发出的请求中获取的数据。在上面的例子当中,这里拿到的数据即为

schema: http

serverName: 1.2.3.4

requestURI: /userinfo/me

当服务端使用这些信息向浏览器发出302,则并不是一个正确的URI。

在spring体系中,可以使用 ForwardedHeaderFilter 来处理此事务,它是1个 filter 层的过滤器,将会读取 X-Forwarded-* 层的头信息,并重新设置在 相应的 request.getXXX 方法上,那么在业务端通过上述这些方法即能拿到正确的数据。 同时,此过滤器也将隐藏X-Forwarded-* 这些请求头,请求信息就像直接从浏览器发过来的一样的。这样一方面避免业务端来手动地处理这些请求,另一方面也避免业务端再次读取这些X-头,然后进行二次处理。

ForwardedHeaderFilter 过滤器并没有默认启动,业务端需要显示地进行启用。

需要注意的是,spring容器中有1个组件 UriComponentsBuilder,以及 ServletUriComponentsBuilder,其内部使用了跟 ForwardedHeaderFilter 一样的逻辑,但是随着版本的变更,其实际逻辑可能就跟 ForwardedHeaderFilter 逻辑不一致的。因此,在没有启用 ForwardedHeaderFilter 的场景中,请先确保 spring 的版本与当前业务期望行为一致。如 spring 5.0.X ServletUriComponentsBuilder 会读取 X-Forwarded-Prefix 头,但spring 5.1.X 就不再处理。其在官方的注释上也写得清楚,如下

<p><strong>Note:</strong> As of 5.1, methods in this class do not extract
{@code "Forwarded"} and {@code "X-Forwarded-*"} headers that specify the
client-originated address. Please, use
{@link org.springframework.web.filter.ForwardedHeaderFilter
ForwardedHeaderFilter}, or similar from the underlying server, to extract
and use such headers, or to discard them.

并且有特定的 https://github.com/spring-projects/spring-hateoas/issues/862 与之挂接.

在实际业务中,如果没有 启用 ForwardedHeaderFilter,但想要拿到正确的请求信息,建议自己参照 ForwardedHeaderFilter中的类 ForwardedHeaderExtractingRequest,处理好信息,产生uri对象之后,再通过 UriComponentsBuilder#fromUri 来拼接地址信息。

Spring Gateway场景

在spring gateway,即Webflux中,也同样有过滤器 reactive.ForwardedHeaderFilter,处理同样的事务,但此过滤器有1个问题,即在处理完之后,相应的 ServerWebExchange.getRequest 拿到的 request中,即与浏览器相一致。即schema将会变为 https(如果浏览器使用https访问)。

一般情况下不会有问题,但当在spring gateway中,使用 lb模式进行路由时(如下写法)

routes:   
    - id: y2b
    uri: lb://y2b

即在 NettyRoutingFilter 构建 下层 request时,将直接 使用 变量 GATEWAY_REQUEST_URL_ATTR 中的 url中的schema值,即此值在这里将为 https,与实际的下层业务协议并不匹配,而产生访问错误问题。

此场景下,需要保证在访问下层服务时一定使用http协议,而不管上层访问协议为什么。这里可以使用类 RouteToRequestUrlFilter的处理手法,即路由协议中显示指定为 http 协议,如下的表示方法

routes:   
    - id: y2b
    uri: lb:http://y2b

此 decodedSchemeSpecificPart 描述地址信息将会尝试将 lb 协议后的字符串重新进行解析,并设置新的协议为 gateway中的 schema值,在后续的中的 GATEWAY_REQUEST_URL_ATTR 中 url的schema即为正确的 http(而不是默认的request中的https). 在这里 url对象将与request表示为不一致的信息。

其它

针对此问题,网上也有其它的处理方法,如 tomcat 自带的 RemoteIpValve, 或者在 ServerProperties 中配置 useForwardHeaders 为 true. 不再的配置值有不同的处理手法。但 针对 特殊头 X-Forwarded-Prefix, 当前仅有 如 spring 及少数应用在尝试处理此值。

在标准的环境下,或者是一个较新的项目(现有代码改动较少),个人建议始终使用 ForwardedHeaderFilter 来处理X-Forwarded-* 头,让业务内无感知。同时,在类似gateway等透传协议下,始终使用显示的处理方法,而不依赖于上下游信息。在某些特殊的场景下(没有使用ForwardedHeaderFilter),甚至自行实现类似 getFullBrowerRequestURI 这样的方法。

原文  https://www.iflym.com/index.php/code/201909070001.html
正文到此结束
Loading...