spring-mvc - Spring RestTemplate 不允许持久的 https 连接

标签 spring-mvc ssl https resttemplate keep-alive

在通过 https 访问 REST api 时,我想通过 Spring RestTemplate 使用持久的 http 连接。我无法让它工作;为每个请求创建一个新连接,并且每次都会进行 SSL 握手。 是否可以通过 https 与 RestTemplate 建立可重用连接?如果可以,如何配置?

我设置了一个 RestTemplate 来通过 https 发出请求。那工作正常。 但是我在日志中注意到每个请求都会发生新的 SSL 握手。

我在测试中设置了一个 RestTemplate,如下所示:

@Before
public void setupPersistentHttpConnectionBackedRestTemplate() {
    final SSLConnectionSocketFactory sslSocketFactory = new SSLConnectionSocketFactory(
        sslContext,
        new String[] { "TLSv1.2" },
        null,
        SSLConnectionSocketFactory.getDefaultHostnameVerifier());
    final Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create()
        .register("https", sslSocketFactory)
        .build();
    final PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(registry);
    final CloseableHttpClient httpClient = HttpClients.custom()
        .setSSLSocketFactory(sslSocketFactory)
        .setConnectionManager(connectionManager)
        .build();
    final HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory();
    requestFactory.setHttpClient(httpClient);

    restTemplate.getRestTemplate().setRequestFactory(requestFactory);
}

然后我像这样使用这个 RestTemplate 进行多次调用:

ResponseEntity<String> response = restTemplate.exchange("/tomcat/sleep?millis={millis}", HttpMethod.GET, HttpEntity.EMPTY, String.class, SLEEP_DURATION);

我调查了 spring-mvc 和 apache 的代码并注意到以下内容。 在Spring RestTemplate的execute方法中,创建一个新的请求,然后执行请求并返回结果。

            ClientHttpRequest request = createRequest(url, method);
            if (requestCallback != null) {
                requestCallback.doWithRequest(request);
            }
            response = request.execute();
            handleResponse(url, method, response);
            return (responseExtractor != null ? responseExtractor.extractData(response) : null);

依次调用 HttpComponentsClientHttpRequestFactory,每次都会创建一个新的 http 上下文:


    @Override
    public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException {
        HttpClient client = getHttpClient();

        HttpUriRequest httpRequest = createHttpUriRequest(httpMethod, uri);
        postProcessHttpRequest(httpRequest);
        HttpContext context = createHttpContext(httpMethod, uri);
        if (context == null) {
            context = HttpClientContext.create();
        }
...

在请求执行调用期间跟踪调用链时,我最终进入了 apache MainClientExec。它会尝试根据路由和上下文用户 token 重用连接。执行请求后,将从上下文中检索用户 token 并存储以供进一步查找。

    @Override
    public CloseableHttpResponse execute(
            final HttpRoute route,
            final HttpRequestWrapper request,
            final HttpClientContext context,
            final HttpExecutionAware execAware) throws IOException, HttpException {
...
        Object userToken = context.getUserToken();

        final ConnectionRequest connRequest = connManager.requestConnection(route, userToken);
...
            if (userToken == null) {
                userToken = userTokenHandler.getUserToken(context);
                context.setAttribute(HttpClientContext.USER_TOKEN, userToken);
            }
            if (userToken != null) {
                connHolder.setState(userToken);
            }
...

在 https 连接的情况下,用户 token 从 SSL 主体获取,而 SSL 主体又从 SSL 证书获取:

    @Override
    public Object getUserToken(final HttpContext context) {
...
        if (userPrincipal == null) {
            final HttpConnection conn = clientContext.getConnection();
            if (conn.isOpen() && conn instanceof ManagedHttpClientConnection) {
                final SSLSession sslsession = ((ManagedHttpClientConnection) conn).getSSLSession();
                if (sslsession != null) {
                    userPrincipal = sslsession.getLocalPrincipal();
                }
            }
        }
    public Principal getLocalPrincipal() {
        if (this.cipherSuite.keyExchange != KeyExchange.K_KRB5 && this.cipherSuite.keyExchange != KeyExchange.K_KRB5_EXPORT) {
            return this.localCerts == null ? null : this.localCerts[0].getSubjectX500Principal();
        } else {
            return this.localPrincipal == null ? null : this.localPrincipal;
        }
    }

PoolingHttpClientConnectionManager 尝试根据路由和状态(已存储用户 token )返回可重用连接。

但是由于 RestTemplate 每次都以一个新的请求和一个新的上下文开始,所以 uset token 丢失并且 PoolingHttpClientConnectionManager 找不到可重用的连接,因此每次都创建一个新的连接。

我希望 RestRemplate 可以创建一个重新使用该连接的请求,而不是每次都创建一个新连接。

最佳答案

我试图实现相同的目标,我看到的唯一方法是扩展 HttpComponentsClientHttpRequestFactory 以设置 UserToken,在本例中为 Principal cert.getSubjectDN() 然后覆盖 createHttpContext(HttpMethod httpMethod, URI uri)

@Override
protected HttpContext createHttpContext(HttpMethod httpMethod, URI uri) {
    HttpClientContext context = HttpClientContext.create();
    context.setUserToken(userToken);
    return context;
}

关于spring-mvc - Spring RestTemplate 不允许持久的 https 连接,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/57201434/

相关文章:

java - Spring MVC 和 UTF-8 : How to work with Swedish special characters?

http - HTTP header 是否区分空格/大小写/顺序?

python - ./manage.py runserver 使用 https

ssl - MAMP PRO 上的 SSL 选项卡显示为灰色

.htaccess - 如何将所有传入请求重定向到 https?

java - 在没有 XML 的 Spring 中配置数据源 URL

java - LazyInitializationException 问题无法初始化代理 - 无 session

spring - 对 API 进行速率限制(spring MVC)

Java 无法从 URL 获取请求的文件

Web 浏览器的 SSL 证书验证