You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.20 00:34:59