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

时间:2018-08-14 23:51:29

标签: java multithreading apache-httpclient-4.x ntlm

我有一个独立的Java客户端,试图通过NTLM代理执行RMI。 它是多线程的。 我正在使用Apache httpclient 4.5.6。 我在5分钟的超时周期内获得了代理。

这种基本情况有效,只要有两个线程在代理超时的同一时间未同时发出请求,代理就会向每5分钟重新进行一次身份验证。然后失败。一旦失败,所有后续尝试都会失败。

我附上了wireshark屏幕截图以进行澄清(屏幕截图来自4.5.2,但我升级到4.5.6并看到了相同的行为)。

wireshark screenshot

一个好的周期看起来像

  • 客户端尝试连接(无NTML标志)
  • 代理服务器回复407(没有NTML标志)
  • 客户端再次尝试使用ntlm消息类型NTLMSSP_NEGOTIATE进行连接
  • 代理服务器回复407 NTLMSSP_CHALLENGE
  • 客户端使用NTLMSSP_AUTH和我的凭据进行连接。
  • 代理回复200,我们很高兴再等5分钟。

一个糟糕的周期看起来像

  • 客户端尝试连接(无NTML标志)
  • 代理服务器回复407(没有NTML标志)
  • 客户端再次尝试使用ntlm消息类型NTLMSSP_NEGOTIATE进行连接
  • 客户端尝试连接(无NTML标志)
  • 代理服务器回复407(没有NTML标志)
  • 代理服务器回复407 NTLMSSP_CHALLENGE
  • 几秒钟之内,就会有更多的连接和407s没有NTML标志。

在我看来,这看起来像是非线程安全代码中的多线程竞争条件。

使用Apache httpclient 4.5.2,它只是传播了407,我在CloseableHttpResponse.getStatusLine()。getStatusCode()中检测到它。 通过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)

有什么主意如何防范或解决它或从中恢复? (除了通话同步,这会大大降低原本已经很慢的应用的速度)

该应用的一些代码段:

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

2 个答案:

答案 0 :(得分:1)

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

答案 1 :(得分:0)

谢谢奥列格(Oleg),这是我的工作,到目前为止,它似乎已经可以正常工作了(太久不能发表评论,但我想分享我的代码)

renderList(){
    const { player } = this.state
    const { players } = this.props
    // ignore user's typing case
    const iPlayer = player.toLowerCase()
    const filteredPlayers = players.filter(p => 
       p.name.toLowerCase().includes(iPlayer)
    )
    // now, map to the filteredPlayers