Android中OKHttpClient配置NTLM认证返回401错误求助
NTLM认证调用始终返回401错误排查方案
使用OkHttpClient结合org.codelibs:jcifs:1.3.18.1库实现NTLM认证时,请求始终返回401错误,无法获取200成功响应。以下是相关代码及排查解决建议:
相关代码
OkHttpClient配置代码(Kotlin)
private fun getOkHttpClient(): OkHttpClient? { return try { val trustAllCerts = arrayOf<TrustManager>( object : X509TrustManager { @Throws(CertificateException::class) override fun checkClientTrusted(chain: Array<X509Certificate?>?, authType: String?) {} @Throws(CertificateException::class) override fun checkServerTrusted(chain: Array<X509Certificate?>?, authType: String?) {} override fun getAcceptedIssuers(): Array<X509Certificate?>? { return arrayOf() } } ) // Install the all-trusting trust manager val sslContext = SSLContext.getInstance("SSL") sslContext.init(null, trustAllCerts, SecureRandom()) // Create an ssl socket factory with our all-trusting manager val sslSocketFactory = sslContext.socketFactory val builder = OkHttpClient.Builder() builder.writeTimeout(60, TimeUnit.SECONDS) builder.readTimeout(60, TimeUnit.SECONDS) builder.connectTimeout(60, TimeUnit.SECONDS) builder.protocols(Util.immutableList(Protocol.HTTP_1_1)) builder.sslSocketFactory(sslSocketFactory, trustAllCerts[0] as X509TrustManager) builder.hostnameVerifier { hostname, session -> true } builder.authenticator(NtlmAuthHelper(Common.USERNAME, Common.PASSWORD)) builder.build() } catch (e: java.lang.Exception) { throw RuntimeException(e) } }
依赖配置
implementation group: 'org.codelibs', name: 'jcifs', version: '1.3.18.1'
NTLM认证辅助类(Java)
public class NtlmAuthHelper implements Authenticator { private static final String CLASS_TAG = "NtlmAuthHelper"; private static final String AUTHENTICATE_HEADERS = "WWW-Authenticate"; private static final String HEADER_NEGOTIATE = "Negotiate"; private static final String HEADER_NTLM = "NTLM"; private static final String HEADER_AUTHORIZATION = "Authorization"; 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; private String m_login; private String m_password; private String m_domain; private String m_workstation; private int m_contaAutenticazioni; private HttpUrl m_url; public NtlmAuthHelper(@NonNull String login, @NonNull String password) { this(login, password, "", ""); } public NtlmAuthHelper(@NonNull String login, @NonNull String password, @NonNull String domain) { this(login, password, domain, ""); } public NtlmAuthHelper(@NonNull String login, @NonNull String password, @NonNull String domain, @NonNull String workstation) { m_login = login; m_password = password; m_domain = domain; m_workstation = workstation; m_contaAutenticazioni = 0; Log.e( "constructor - m_cont " , String.valueOf(m_contaAutenticazioni)); Log.e( "constructor - m_login " , m_login); Log.e( "constructor - m_pass " , m_password); } @Override public Request authenticate(Route route, Response response) throws IOException { if (m_url == null || response.request().url() != m_url) { m_contaAutenticazioni = 0; m_url = response.request().url(); } m_contaAutenticazioni++; if (m_contaAutenticazioni >= 4) { throw new IOException(String.format("challenge request count too big (%s)",m_contaAutenticazioni)); } List<String> authHeaders = response.headers(AUTHENTICATE_HEADERS); if (authHeaders != null) { boolean negotiate = false; boolean ntlm = false; String ntlmValue = null; for (String authHeader : authHeaders) { if (authHeader.equalsIgnoreCase(HEADER_NEGOTIATE)) { negotiate = true; Log.e( "constructor - 3 " , String.valueOf(response.code())); } if (authHeader.equalsIgnoreCase(HEADER_NTLM)) { ntlm = true; Log.e( "constructor - 4 " , String.valueOf(response.code())); } if (authHeader.startsWith(HEADER_NTLM + " ")) { ntlmValue = authHeader.substring(5); Log.e( "constructor - 5 " , String.valueOf(response.code())); } } if (negotiate && ntlm) { String type1Msg = generateType1Msg(m_domain, m_workstation); String header = HEADER_NTLM + " " + type1Msg; Log.v(CLASS_TAG, HEADER_AUTHORIZATION + " " + header); Log.e( "constructor - 6 " , String.valueOf(response.code())); return response.request().newBuilder().header(HEADER_AUTHORIZATION, header).build(); } else if (ntlmValue != null) { String type3Msg = generateType3Msg(m_login, m_password, m_domain, m_workstation, ntlmValue); String ntlmHeader = HEADER_NTLM + " " + type3Msg; Log.v(CLASS_TAG, HEADER_AUTHORIZATION + " " + ntlmHeader); Log.e( "constructor - 7 " , String.valueOf(response.code())); return response.request().newBuilder().header(HEADER_AUTHORIZATION, ntlmHeader).build(); } } if (responseCount(response) <= 3) { String credential = Credentials.basic(m_login, m_password); Log.v(CLASS_TAG, HEADER_AUTHORIZATION + " " + credential); Log.e( "constructor - cred " , String.valueOf(response.code())); return response.request().newBuilder().header(HEADER_AUTHORIZATION, credential).build(); } return null; } private String generateType1Msg(@NonNull String domain, @NonNull String workstation) { final Type1Message type1Message = new Type1Message(TYPE_1_FLAGS, domain, workstation); byte[] source = type1Message.toByteArray(); Log.e( "constructor - Msg1 " , "MSG1"); return Base64.encode(source); } private String generateType3Msg(final String login, final String password, final String domain, final String workstation, final String challenge) { Type2Message type2Message; try { byte[] decoded = Base64.decode(challenge); type2Message = new Type2Message(decoded); Log.e( "constructor - Msg2 " , "MSG2"); } catch (final IOException exception) { exception.printStackTrace(); Log.e( "constructor - Msg3 " , "MSG3"); return null; } final int type2Flags = type2Message.getFlags(); final int type3Flags = type2Flags & ~(NtlmFlags.NTLMSSP_TARGET_TYPE_DOMAIN | NtlmFlags.NTLMSSP_TARGET_TYPE_SERVER); final Type3Message type3Message = new Type3Message(type2Message, password, domain, login, workstation, type3Flags); Log.e( "constructor - Msg4 " , "MSG4"); return Base64.encode(type3Message.toByteArray()); } private int responseCount(Response response) { int result = 1; while ((response = response.priorResponse()) != null) { Log.e( "constructor - Redirect " , "" +response.isRedirect()); if (!response.isRedirect()) result++; } return result; } }
排查与解决建议
- 修正URL匹配逻辑:当前使用
response.request().url() != m_url进行对象引用比较,改为值比较!response.request().url().equals(m_url),避免因URL对象实例不同导致计数器错误重置。 - 调整Type1消息标志:部分旧版NTLM服务器不支持NTLMv2,移除
NtlmFlags.NTLMSSP_NEGOTIATE_NTLM2标志,尝试使用NTLMv1兼容模式:private static final int TYPE_1_FLAGS = NtlmFlags.NTLMSSP_NEGOTIATE_56 | NtlmFlags.NTLMSSP_NEGOTIATE_128 | NtlmFlags.NTLMSSP_NEGOTIATE_ALWAYS_SIGN | NtlmFlags.NTLMSSP_REQUEST_TARGET; - 提供正确的域名/工作站信息:避免传入空字符串,传入实际的域名称或本地工作站名称,部分服务器要求这些参数非空才能完成认证。
- 统一Base64编码格式:替换jcifs的
Base64.encode为系统标准的Base64.encodeToString并指定Base64.NO_WRAP,确保HTTP头中无换行符干扰:// 修改generateType1Msg返回 return Base64.encodeToString(source, Base64.NO_WRAP); // 修改generateType3Msg返回 return Base64.encodeToString(type3Message.toByteArray(), Base64.NO_WRAP); - 简化认证分支逻辑:暂时注释掉Basic认证的分支代码,专注排查NTLM协议交互流程,避免Basic认证干扰NTLM的重试逻辑。
- 升级jcifs依赖版本:1.3.18.1版本较旧,升级到
org.codelibs:jcifs-ng:2.1.32,新版本修复了大量NTLM协议兼容性问题。 - 校验NTLM消息交互流程:抓包确认Type1、Type2、Type3消息是否完整发送与接收,检查服务器返回的Type2消息是否被正确解析,以及Type3消息的生成是否符合服务器要求。
内容的提问来源于stack exchange,提问作者Govind Rastogi
相关产品推荐
相关产品推荐

