曲線救國,解決spring-boot2.0.6中webflux無法獲得請求IP的問題

since1986發表於2019-03-03

前言

這幾天在用 spring-boot 2webflux 重構一個工程,寫到了一個需要獲得客戶端請求 IP 的地方,發現寫不下去了,在如下的 Handler(webflux 中 Handler 相當於 mvc 中的 Controller)中

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;

import static org.springframework.web.reactive.function.server.RequestPredicates.GET;
import static org.springframework.web.reactive.function.server.RequestPredicates.accept;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;

/**
 * 某業務 Handler
 */
@Component
public class SplashHandler {

    private Mono<ServerResponse> execute(ServerRequest serverRequest) {
        ... 業務程式碼
        // serverRequest 獲得 IP ?
        ... 業務程式碼
    }

    @Configuration
    public static class RoutingConfiguration {

        @Bean
        public RouterFunction<ServerResponse> execute(SplashHandler handler) {
            return route(
                    GET("/api/ad").and(accept(MediaType.TEXT_HTML)),
                    handler::execute
            );
        }
    }
}

複製程式碼

我發現 org.springframework.web.reactive.function.server.ServerRequest 根本沒有暴露用於獲得客戶端 IP 的 API,想想這在傳統 MVC 中是相當基本的需求啊,竟然獲取不到,然後 Google 了一下,發現這個是 spring-webflux 的一個 BUG,這個 BUG 在 spring-webflux 5.1 中解決了,但是,略有些尷尬的是當前最新穩定版的 spring-boot 還是依賴 5.0.x 的 spring-webflux 的。難道要等官方升級麼,那不知道得等到什麼時候,因此我接著搜了搜資料,看了看文件和原始碼,自己想了個曲線救國的辦法。

正文

spring-webflux 中,有一個 org.springframework.web.server.WebFilter 介面,類似於 Servlet API 中的過濾器,這個 API 提供了一個方法會將一個限定名為 org.springframework.web.server.ServerWebExchange 的類暴露出來,而在這個類中就包含了對於請求端 IP 的獲取方法:

org.springframework.web.server.ServerWebExchange#getRequest
org.springframework.http.server.reactive.ServerHttpRequest#getRemoteAddress
複製程式碼

因此,我們大可以實現一個 WebFilter 在裡面通過暴露的 ServerWebExchange 拿到客戶端 IP,然後再將其塞到請求的 header 中,這樣,後續過程就可以從 header 中取 IP 了。思路有了,我們開始實現吧。

過濾、取 IP、放 header,一氣呵成:

import org.springframework.context.annotation.Configuration;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.config.CorsRegistry;
import org.springframework.web.reactive.config.WebFluxConfigurer;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;

import java.net.InetSocketAddress;
import java.util.Objects;

/*
If you want to keep Spring Boot WebFlux features and you want to add additional WebFlux configuration, you can add your own @Configuration class of type WebFluxConfigurer but without @EnableWebFlux.
If you want to take complete control of Spring WebFlux, you can add your own @Configuration annotated with @EnableWebFlux.
 */
@Configuration
public class WebConfiguration implements WebFluxConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry
                .addMapping("/**")
                .allowedOrigins("*")
                .allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTION")
                .allowedHeaders("header1", "header2", "header3")
                .exposedHeaders("header1", "header2")
                .allowCredentials(true)
                .maxAge(3600);
    }

    /**
     * https://stackoverflow.com/questions/51192630/how-do-you-get-clients-ip-address-spring-webflux-websocket?rq=1
     * https://stackoverflow.com/questions/50981136/how-to-get-client-ip-in-webflux
     * https://docs.spring.io/spring/docs/current/spring-framework-reference/web-reactive.html#webflux-filters
     * 由於在低版本的 spring-webflux 中不支援直接獲得請求 IP(https://jira.spring.io/browse/SPR-16681),因此寫了一個補丁曲線救國,
     * 從 org.springframework.web.server.ServerWebExchange 中獲得 IP 後,在放到 header 裡
     */
    @Component
    public static class RetrieveClientIpWebFilter implements WebFilter {

        @Override
        public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
            InetSocketAddress remoteAddress = exchange.getRequest().getRemoteAddress();
            String clientIp = Objects.requireNonNull(remoteAddress).getAddress().getHostAddress();
            ServerHttpRequest mutatedServerHttpRequest = exchange.getRequest().mutate().header("X-CLIENT-IP", clientIp).build();
            ServerWebExchange mutatedServerWebExchange = exchange.mutate().request(mutatedServerHttpRequest).build();
            return chain.filter(mutatedServerWebExchange);
        }
    }
}

複製程式碼

後續過程 header 取值:

    private Mono<ServerResponse> execute(ServerRequest serverRequest) {
        String clientIp = serverRequest.headers().asHttpHeaders().getFirst("X-CLIENT-IP")
        ... 業務程式碼
    }

複製程式碼

通過上述解決方案(其實嚴格上說是 hacking)就解決了我們遇到的問題了。

參考資料

How do you get Client`s IP address? (Spring WebFlux WebSocket)

How to get client IP in webflux?

spring-webflux 官方文件第 1.2.3. 小節 `Filters`

Spring FrameworkSPR-16681 (jira)

相關文章