spring-cloud-gateway icon indicating copy to clipboard operation
spring-cloud-gateway copied to clipboard

Disable Apache client5 cookie manager by default

Open florentm35 opened this issue 11 months ago • 4 comments

Describe the bug We have a tomcat between the gateway MVC (spring cloud 2023.0.0), some problem occured with the JDK httpclient (in java 17 they are problem with node), so we use the implementation of apache httpclient, our tomcat use cookie to pass the JSESSION ID (who is use for the session).

The problem is that the httpclient have the cookie manager activated and the httpclient are reused for all request.

I see two ways to correct this issue :

  • Disable the cookie manager of the httpclient
  • Recreate a new httpclient for all request

How to reproduce if we test with postman and the http interceptor with gateway mvc first call, http-interceptio log :

/test
Generated cookie : 2143473103

on postman we removed all cookie, and the second call :

/test
Request cookie : [a=2143473103]
Generated cookie : 1554856040

The previous cookie was found in the interceptor, because when the first call occured, the cookie in the response was add to the cookie manager of the httpclient, and is reused for the next request

Sample HTTP interceptor for test :

import com.sun.net.httpserver.HttpServer;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.Random;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;

public class Main {

    public static void main(String[] args) throws IOException {

        ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) Executors.newFixedThreadPool(10);

        HttpServer server = HttpServer.create(new InetSocketAddress("localhost", 9008), 0);
        server.createContext("/", exchange -> {
            System.out.println(exchange.getRequestURI());

            exchange.getRequestHeaders().entrySet().stream().filter(e -> e.getKey().toLowerCase().equals("cookie")).forEach(e -> System.out.println("Request cookie : " + e.getValue()));

            Random random = new Random();
            int cookie = random.nextInt();

            System.out.println("Generated cookie : " + cookie);

            exchange.getResponseHeaders().add("Set-Cookie", "a="+cookie);
            exchange.getResponseHeaders().add("a", "b");

            exchange.sendResponseHeaders(200, "OK".length());
            exchange.getResponseBody().write("OK".getBytes(StandardCharsets.UTF_8));

            exchange.getResponseBody().close();
        });
        server.setExecutor(threadPoolExecutor);
        server.start();
        System.out.println(" Server started on port 9008");
    }
}

gateway MVC : (spring initializr ) Main class :

@SpringBootApplication
public class DemoApplication {

	public static void main(String[] args) {
		SpringApplication.run(DemoApplication.class, args);
	}

	@Configuration
	public static class GatewayConfiguration {

		@Bean
		public RouterFunction<ServerResponse> routerFunction() {
			return route()
					.POST("/test", http("http://localhost:9008"))
					.build();
		}
	}
}

In maven we add :

<dependency>
	<groupId>org.apache.httpcomponents.client5</groupId>
	<artifactId>httpclient5</artifactId>
	<version>5.3.1</version>
</dependency>

And in the application.properties we add : spring.cloud.gateway.mvc.httpclient.type=AUTODETECT

florentm35 avatar Mar 20 '24 09:03 florentm35

Disable the cookie manager of the httpclient

I think this is the only option.

spencergibb avatar Mar 20 '24 15:03 spencergibb

Yes it's our method for the moment, but the code are really ugly :

/** GatewayServerMvcAutoConfiguration.gatewayClientHttpRequestFactory */
    @Bean
    public ClientHttpRequestFactory gatewayClientHttpRequestFactory(GatewayMvcProperties gatewayMvcProperties,
                                                                    SslBundles sslBundles) {
        GatewayMvcProperties.HttpClient properties = gatewayMvcProperties.getHttpClient();

        SslBundle sslBundle = null;
        if (org.springframework.util.StringUtils.hasText(properties.getSslBundle())) {
            sslBundle = sslBundles.getBundle(properties.getSslBundle());
        }
        ClientHttpRequestFactorySettings settings = new ClientHttpRequestFactorySettings(properties.getConnectTimeout(),
                properties.getReadTimeout(), sslBundle);

        ClientHttpRequestFactory chrf;
        if (properties.getType() == GatewayMvcProperties.HttpClientType.JDK) {
            // TODO: customize restricted headers
            String restrictedHeaders = System.getProperty("jdk.httpclient.allowRestrictedHeaders");
            if (!org.springframework.util.StringUtils.hasText(restrictedHeaders)) {
                System.setProperty("jdk.httpclient.allowRestrictedHeaders", "host");
            }
            else if (org.springframework.util.StringUtils.hasText(restrictedHeaders) && !restrictedHeaders.contains("host")) {
                System.setProperty("jdk.httpclient.allowRestrictedHeaders", restrictedHeaders + ",host");
            }

            chrf = ClientHttpRequestFactories.get(JdkClientHttpRequestFactory.class, settings);
        } else {
            chrf = ClientHttpRequestFactories.get(settings);
        }

        log.error("chrf : {}", chrf);
        if (chrf instanceof HttpComponentsClientHttpRequestFactory hcchrf) {
            log.error("hcchrf.getHttpClient() : {}", hcchrf.getHttpClient());
        } else if (chrf instanceof JdkClientHttpRequestFactory jchrf) {
            try {
                Field field = jchrf.getClass().getDeclaredField("httpClient");
                field.setAccessible(true);
                Object httpClient = field.get(jchrf);
                log.error("jchrf.getHttpClient() : {}", httpClient);
            } catch (Exception e) {
                log.error("", e);
            }
        }

       if (chrf instanceof HttpComponentsClientHttpRequestFactory hcchrf) {
            hcchrf.setHttpClient(createHttpClient(settings.readTimeout(), settings.sslBundle()));
        }

        return chrf;
    }

    private static HttpClient createHttpClient(Duration readTimeout, SslBundle sslBundle) {
        PoolingHttpClientConnectionManagerBuilder connectionManagerBuilder = PoolingHttpClientConnectionManagerBuilder
                .create();
        if (readTimeout != null) {
            SocketConfig socketConfig = SocketConfig.custom()
                    .setSoTimeout((int) readTimeout.toMillis(), TimeUnit.MILLISECONDS)
                    .build();
            connectionManagerBuilder.setDefaultSocketConfig(socketConfig);
        }
        if (sslBundle != null) {
            SslOptions options = sslBundle.getOptions();
            SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory(sslBundle.createSslContext(),
                    options.getEnabledProtocols(), options.getCiphers(), new DefaultHostnameVerifier());
            connectionManagerBuilder.setSSLSocketFactory(socketFactory);
        }
        PoolingHttpClientConnectionManager connectionManager = connectionManagerBuilder.useSystemProperties()
                .build();
       // We disabled the cookie management here
        return HttpClientBuilder.create().useSystemProperties().disableCookieManagement().setConnectionManager(connectionManager).build();
    }

florentm35 avatar Mar 20 '24 16:03 florentm35

#3305 will simplify that.

spencergibb avatar Mar 20 '24 16:03 spencergibb

It's seen more for spring cloud gateway mvc to disable it by default, not the user

florentm35 avatar Mar 20 '24 16:03 florentm35