简体   繁体   English

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

[英]multithread issue with NTLM and Java Apache httpclient 4.5.6

I have a stand-alone Java client trying to do RMI through a NTLM proxy. 我有一个独立的Java客户端,试图通过NTLM代理执行RMI。 It's multithreaded. 它是多线程的。 I'm using Apache httpclient 4.5.6. 我正在使用Apache httpclient 4.5.6。 I've got the proxy on a 5 minute timeout cycle. 我有一个5分钟的超时周期的代理。

The basic case works, reauthenticating every 5 minutes when challenged by the proxy, as long as 2 threads don't make a request at the same time at exactly the time the proxy times out. 基本情况是可行的,只要受到代理的挑战,每5分钟就会重新进行身份验证,只要两个线程在代理超时的确切时间未同时发出请求即可。 Then it fails. 然后失败。 Once it fails, all subsequent attempts fail. 一旦失败,所有后续尝试都会失败。

I've attached a wireshark screenshot to clarify (screenshot is from 4.5.2 but I upgraded to 4.5.6 and saw the same behavior). 我附上了wireshark屏幕截图以进行澄清(屏幕截图来自4.5.2,但我升级到4.5.6并看到了相同的行为)。

Wireshark屏幕截图

A good cycle looks like 一个好的周期看起来像

  • Client tries CONNECT (no NTML flags) 客户端尝试连接(无NTML标志)
  • Proxy replies with 407 (no NTML flags) 代理回复407(无NTML标志)
  • Client tries CONNECT again with ntlm messagetype NTLMSSP_NEGOTIATE 客户端再次尝试使用ntlm消息类型NTLMSSP_NEGOTIATE进行CONNECT
  • Proxy replies with 407 NTLMSSP_CHALLENGE 代理回复407 NTLMSSP_CHALLENGE
  • Client does CONNECT with NTLMSSP_AUTH and my credentials. 客户端确实使用NTLMSSP_AUTH和我的凭据进行连接。
  • Proxy replies with 200, and we are good to go for another 5 minutes. 代理回复200,我们很高兴再继续5分钟。

A bad cycle looks like 糟糕的周期看起来像

  • Client tries CONNECT (no NTML flags) 客户端尝试连接(无NTML标志)
  • Proxy replies with 407 (no NTML flags) 代理回复407(无NTML标志)
  • Client tries CONNECT again with ntlm messagetype NTLMSSP_NEGOTIATE 客户端再次尝试使用ntlm消息类型NTLMSSP_NEGOTIATE进行CONNECT
  • Client tries CONNECT (no NTML flags) 客户端尝试连接(无NTML标志)
  • Proxy replies with 407 (no NTML flags) 代理回复407(无NTML标志)
  • Proxy replies with 407 NTLMSSP_CHALLENGE 代理回复407 NTLMSSP_CHALLENGE
  • A whole bunch more CONNECTs and 407s without NTML flags within a few seconds. 几秒钟之内,便有大量的不带NTML标志的CONNECT和407。

to me this looks like a multithread race condition in non-threadsafe code. 对我来说,这看起来像非线程安全代码中的多线程竞争条件。

With Apache httpclient 4.5.2 it just propogated the 407 and I detected it in CloseableHttpResponse.getStatusLine().getStatusCode(). 使用Apache httpclient 4.5.2,它只是传播了407,我在CloseableHttpResponse.getStatusLine()。getStatusCode()中检测到它。 With Apache httpclient 4.5.6 I see this 使用Apache httpclient 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)

Any ideas how to protect against this or work around it or recover from it? 有什么想法可以防止这种情况发生或如何解决或从中恢复吗? (beside sync on the calls, which would slow down an already slow app a lot) (除了同步通话,这会大大降低已经很慢的应用的运行速度)

some code snippets from the app: 该应用程序的一些代码段:

// 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 instances are perfectly thread-safe. HttpContext实例是完全线程安全的。 However some attributes stored in the context such as authentication handshake state are obviously not. 但是,存储在上下文中的某些属性(如身份验证握手状态)显然不是。 Make sure that HttpContext instances do not get updated concurrently and the problem should go away. 确保HttpContext实例不会同时更新,并且该问题应该消失。

Thank you Oleg, this is what I did and it seems to be working so far (too long to post as a comment on your answer, but I wanted to share my code) 谢谢奥列格(Oleg),这是我所做的,到目前为止,它似乎一直在起作用(对于您的答案发表评论的时间太长,但我想分享我的代码)

// 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());

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM