NTLM和Java Apache httpclient 4.5.6的多线程问题

NTLM和Java Apache httpclient 4.5.6的多线程问题,java,multithreading,apache-httpclient-4.x,ntlm,Java,Multithreading,Apache Httpclient 4.x,Ntlm,我有一个独立的Java客户机试图通过NTLM代理进行RMI。 它是多线程的。 我正在使用ApacheHttpClient 4.5.6。 我有一个5分钟超时周期的代理 基本情况下,只要两个线程没有在代理超时的同时发出请求,当代理发出质询时,每5分钟重新验证一次。然后它失败了。一旦失败,所有后续尝试都将失败 我附上了一个wireshark截图来澄清(截图来自4.5.2,但我升级到了4.5.6,看到了相同的行为) 一个好的循环看起来像 客户端尝试连接(无NTML标志) 使用407(无NTML标志)

我有一个独立的Java客户机试图通过NTLM代理进行RMI。 它是多线程的。 我正在使用ApacheHttpClient 4.5.6。 我有一个5分钟超时周期的代理

基本情况下,只要两个线程没有在代理超时的同时发出请求,当代理发出质询时,每5分钟重新验证一次。然后它失败了。一旦失败,所有后续尝试都将失败

我附上了一个wireshark截图来澄清(截图来自4.5.2,但我升级到了4.5.6,看到了相同的行为)

一个好的循环看起来像

  • 客户端尝试连接(无NTML标志)
  • 使用407(无NTML标志)的代理答复
  • 客户端尝试使用ntlm消息类型NTLMSSP\u再次连接
  • 代理答复407 NTLMSSP_质询
  • 客户端确实与NTLMSSP_AUTH和我的凭据连接
  • 代理回复200,我们可以再继续5分钟
坏循环看起来像

  • 客户端尝试连接(无NTML标志)
  • 使用407(无NTML标志)的代理答复
  • 客户端尝试使用ntlm消息类型NTLMSSP\u再次连接
  • 客户端尝试连接(无NTML标志)
  • 使用407(无NTML标志)的代理答复
  • 代理答复407 NTLMSSP_质询
  • 在几秒钟内,更多的连接和407没有NTML标志
对我来说,这看起来像是非线程安全代码中的多线程竞争条件

在ApacheHttpClient 4.5.2中,它刚刚发布了407,我在CloseableHttpResponse.getStatusLine().getStatusCode()中检测到了它。 对于ApacheHttpClient 4.5.6,我看到了这一点

java.lang.IllegalStateException: Auth scheme is null
    at org.apache.http.util.Asserts.notNull(Asserts.java:52)
    at org.apache.http.impl.auth.HttpAuthenticator.ensureAuthScheme(HttpAuthenticator.java:229)
    at org.apache.http.impl.auth.HttpAuthenticator.generateAuthResponse(HttpAuthenticator.java:184)
    at org.apache.http.impl.execchain.MainClientExec.createTunnelToTarget(MainClientExec.java:484)
    at org.apache.http.impl.execchain.MainClientExec.establishRoute(MainClientExec.java:411)
    at org.apache.http.impl.execchain.MainClientExec.execute(MainClientExec.java:237)
    at org.apache.http.impl.execchain.ProtocolExec.execute(ProtocolExec.java:185)
    at org.apache.http.impl.execchain.RetryExec.execute(RetryExec.java:89)
    at org.apache.http.impl.execchain.RedirectExec.execute(RedirectExec.java:110)
    at org.apache.http.impl.client.InternalHttpClient.doExecute(InternalHttpClient.java:185)
    at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:83)
有什么办法可以防止这种情况发生,或者解决它,或者从中恢复过来? (除了通话同步之外,这会大大降低已经很慢的应用程序的速度)

应用程序中的一些代码片段:

// this is done only once
HttpClientBuilder builder = HttpClients.custom();
SocketConfig.Builder socketConfig = SocketConfig.custom();
RequestConfig.Builder requestConfig = RequestConfig.custom();
HttpHost proxy = new HttpHost(proxyHost, proxyPort);
builder.setProxy(proxy);
requestConfig.setProxy(proxy);
builder.setProxyAuthenticationStrategy(new ProxyAuthenticationStrategy());
CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
String localHost = getLocalHostname();
credentialsProvider.setCredentials(
    new AuthScope(proxyHost, proxyPort, AuthScope.ANY_REALM, "ntlm"),
    new NTCredentials(user, password, localHost, domain));
builder.setDefaultCredentialsProvider(credentialsProvider);
builder.setDefaultSocketConfig(socketConfig.build());
builder.setDefaultRequestConfig(requestConfig.build());
CloseableHttpClient client = builder.build();

...

// cached, we use the same one every time in accordance with section 4.7 of
// https://hc.apache.org/httpcomponents-client-4.5.x/tutorial/html/authentication.html
HttpClientContext context = HttpClientContext.create();
context.setCredentialsProvider(credentialsProvider);

...

// new HttpPost every time
HttpPost postMethod = new HttpPost(uri);
postMethod.setEntity(new ByteArrayEntity(bytesOut.toByteArray()));
response = client.execute(postMethod, context);

HttpContext
实例是完全线程安全的。但是,存储在上下文中的某些属性(如身份验证握手状态)显然不是。确保
HttpContext
实例不会同时更新,问题应该会消失

谢谢你,Oleg,这就是我所做的,到目前为止它似乎还有效(太长了,无法对你的答案发表评论,但我想分享我的代码)

//我在不通过代理时使用基本版本
公共类HttpClientContextFactory{
公共HttpClientContext创建(){
返回HttpClientContext.create();
}
}
//我在通过NTLM代理时使用此选项
私有HttpClientContextFactory getNtlmContextFactory(
最终证书提供者(证书提供者){
返回新的HttpClientContextFactory(){
ThreadLocal tlContext=ThreadLocal
.带首字母(()->{
HttpClientContext=HttpClientContext.create();
context.setCredentialsProvider(credentialsProvider);
返回上下文;
});
@凌驾
公共HttpClientContext创建(){
返回tlContext.get();
}
};
}
//然后在连接到服务器时执行此操作
response=client.execute(postMethod,contextFactory.create());

多个线程同时使用了
HttpClientContext
吗?是。的第4.7节说始终使用同一个线程,尽管它没有提到线程。不要。我如何确保这一点?我的代码没有更新
HttpClientContext
,这发生在apachehttpclient代码的深处。我对上下文所做的唯一一件事就是将其传递到
CloseableHttpClient.execute(HttpUriRequest,HttpContext)
谢谢,这就是我所做的,到目前为止它似乎还在工作。
// I use the base version when not going through a proxy
public class HttpClientContextFactory {
    public HttpClientContext create() {
        return HttpClientContext.create();
    }
}

// I use this when I go through a NTLM proxy
private HttpClientContextFactory getNtlmContextFactory(
        final CredentialsProvider credentialsProvider) {
    return new HttpClientContextFactory() {
        ThreadLocal<HttpClientContext> tlContext = ThreadLocal
                .<HttpClientContext> withInitial(() -> {
                    HttpClientContext context = HttpClientContext.create();
                    context.setCredentialsProvider(credentialsProvider);
                    return context;
                });

        @Override
        public HttpClientContext create() {
            return tlContext.get();
        }
    };
}

// then do this when I connect to the server
response = client.execute(postMethod, contextFactory.create());