最近用HttpClient做一个工具做代理访问服务器的WebService,由于服务器是采用NTLM验证的,再加上网上的很多资料大多是关于HttpClient 3的,折磨了好久才搞定。这里记录一下,供以后参考使用。
关于文中内容,我不按照手册上的写法,而是整理成小Tips,希望看起来简单轻松一点。
1, HttpClient 4.x 库可以自己处理Cookie
这一点误导了我很久,HttpClient 4.x提供了CookieStore来帮助程序员管理本地Cookie,并且在Http请求的过程中,如果服务器的返回中带有Set-Cookie这样的字段,当前HttpClient的CookieStore中就会增加一条新的Cookie。
(注意:误导来了)很多网上的资料认为即便当前的HttpClient的CookieStore中有服务器返回的Cookie,HttpClient在下次请求的时候也不会主动加上,需要用户自己手动添加,添加的方法有两种,一种是直接SetHeader,一种是设置CookieStore,比如:
if (!httpclient.getCookieStore().getCookies().isEmpty()) {
Cookie getCookie = httpclient.getCookieStore().getCookies().get(0);
BasicCookieStore cookieStore = new BasicCookieStore();
cookieStore.addCookie(getCookie);
httpclient.setCookieStore(cookieStore);
}
但事实是:
HttpClient是否在下次请求中携带从服务器端请求来的Cookie,完全是由设置决定的。
httpclient.getParams.setParameter(ClientPNames.COOKIE_POLICY, CookiePolicy.BEST_MATCH)
如果设置为Cookie策略为BEST_MATCH的话,HttpClient会在请求中携带由服务器返回的Cookie。如果按照上面的写法,手动添加了CookieStore,那么就会在下次请求中夹带着两个Cookie,Cookie和Cookie2。
2,NTLM如何使用HttpClient模拟
NTLM分为很多很多版本,HttpClient 3.x是支持NTLMv1,而因为legal的原因,HttpClient 4.0.x不再支持,转而使用jcifs库来支持NTLM,但是估计还是只支持v1,而没有继续扩展支持更高级的验证身份。
如何使用NTLM验证呢?首先NTLM过程至少要3次请求才能完成一个资源的请求,我们不需要手动模拟这三个请求,只需要使用NTLM的功能就可以了。
配置如下:
httpclient = new DefaultHttpClient();
httpclient.getAuthSchemes().register("ntlm", new NTLMSchemeFactory());
httpclient.getCredentialsProvider().setCredentials(AuthScope.ANY,
new NTCredentials(asi.getUser(), asi.getPass(), asi.SERVER_NAME, asi.getDomain()));
List<String> authpref = new ArrayList<String>();
authpref.add(AuthPolicy.NTLM);
httpclient.getParams().setParameter(AuthPNames.TARGET_AUTH_PREF, authpref);
其中的NTLMSchemeFactory实现如下:
package ntml;
import org.apache.http.auth.AuthScheme;
import org.apache.http.auth.AuthSchemeFactory;
import org.apache.http.impl.auth.NTLMScheme;
import org.apache.http.params.HttpParams;
public class NTLMSchemeFactory implements AuthSchemeFactory {
public AuthScheme newInstance(final HttpParams params) {
return new NTLMScheme(new JCIFSEngine());
}
}
package ntml;
import jcifs.ntlmssp.NtlmFlags;
import jcifs.ntlmssp.Type1Message;
import jcifs.ntlmssp.Type2Message;
import jcifs.ntlmssp.Type3Message;
import jcifs.util.Base64;
import org.apache.http.impl.auth.NTLMEngine;
import org.apache.http.impl.auth.NTLMEngineException;
import java.io.IOException;
public final class JCIFSEngine implements NTLMEngine {
private static final int TYPE_1_FLAGS =
NtlmFlags.NTLMSSP_NEGOTIATE_56 |
NtlmFlags.NTLMSSP_NEGOTIATE_128 |
NtlmFlags.NTLMSSP_NEGOTIATE_NTLM2 |
NtlmFlags.NTLMSSP_NEGOTIATE_ALWAYS_SIGN |
NtlmFlags.NTLMSSP_REQUEST_TARGET;
public String generateType1Msg(final String domain, final String workstation)
throws NTLMEngineException {
final Type1Message type1Message = new Type1Message(TYPE_1_FLAGS, domain, workstation);
return Base64.encode(type1Message.toByteArray());
}
public String generateType3Msg(final String username, final String password,
final String domain, final String workstation, final String challenge)
throws NTLMEngineException {
Type2Message type2Message;
try {
type2Message = new Type2Message(Base64.decode(challenge));
} catch (final IOException exception) {
throw new NTLMEngineException("Invalid NTLM type 2 message", exception);
}
final int type2Flags = type2Message.getFlags();
final int type3Flags = type2Flags
& (0xffffffff ^ (NtlmFlags.NTLMSSP_TARGET_TYPE_DOMAIN | NtlmFlags.NTLMSSP_TARGET_TYPE_SERVER));
final Type3Message type3Message = new Type3Message(type2Message, password, domain,
username, workstation, type3Flags);
return Base64.encode(type3Message.toByteArray());
}
}
3, NTLM中的Cookie问题
由于NTLM的几个过程中,我们无法插一脚,所以实际的NTLM认证过程中,对于哪些服务器产生产生Cookie,并要求客户端携Cookie请求以让服务器绑定Session的情况,如果不利用tips 1里面的自动携带Cookie,可能就不能做什么了。这也是前一段时间卡住最主要的内容。
4,下一阶段准备尝试使用HttpClient编写一个库来爬Baidu或者douban的音乐...已经有Python的了,哈哈。。。