Android 中使用 HttpURLConnection 的摘要式身份验证

Digest authentication in Android using HttpURLConnection

正如问题所说,我正尝试在 android 中进行摘要式身份验证。
到目前为止,我一直使用 DefaultHttpClient 及其身份验证方法(使用 UsernamePasswordCredentials 等),但自 Android 5 起已弃用,并将在 Android 6 中删除。
所以我即将从 DefaultHttpClient 切换到 HttpUrlConnection
现在我正在尝试实现摘要身份验证,它应该像 here:

所解释的那样工作起来非常简单
Authenticator.setDefault(new Authenticator() {
    protected PasswordAuthentication getPasswordAuthentication() {
        return new PasswordAuthentication(username, password);
    }
});

但是 getPasswordAuthentication 由于某种原因从未被调用过。
在我搜索这个问题的过程中,我发现了不同的帖子,说 android 中的 HttpUrlConnection 不支持摘要身份验证,但这些帖子来自 2010-2012,所以我不确定这是否仍然正确.此外,我们在我们的桌面 java 应用程序中使用 HttpUrlConnection 和摘要身份验证,它确实有效。

我也找了一些帖子,说的是OkHttpOkHttp 似乎被 Android 在幕后使用(更具体地说是 HttpUrlConnectionImpl)。但是这个 HttpUrlConnectionImpl 有点奇怪,它甚至没有显示在 Eclipse 类型层次结构中,我无法调试它。它也应该是 com.squareup.okhttp.internal.huc.HttpUrlConnectionImpl,而在 android 中它是 com.android.okhttp.internal.http.HttpUrlConnectionImpl

所以我无法使用 android 中的 HttpUrlConnection 进行摘要式身份验证。
谁能告诉我如何在没有外部库的情况下做到这一点?

编辑:
服务器请求摘要认证:

WWW-Authenticate: Digest realm="Realm Name",domain="/domain",nonce="nonce",algorithm=MD5,qop="auth"

所以基本身份验证应该工作,因为服务器正在请求摘要。

您是否尝试像这样手动设置页眉:

String basic = "Basic " + new String(Base64.encode("username:password".getBytes(),Base64.NO_WRAP ));
connection.setRequestProperty ("Authorization", basic);

还请注意 Jellybeans 中的一些问题以及尝试执行 post 请求时的错误:

编辑:摘要式身份验证

看这里https://code.google.com/p/android/issues/detail?id=9579

特别是这可能有效:

try {   
        HttpClient client = new HttpClient(
                new MultiThreadedHttpConnectionManager());

        client.getParams().setAuthenticationPreemptive(true);
        Credentials credentials = new UsernamePasswordCredentials("username", "password");
        client.getState().setCredentials(AuthScope.ANY, credentials);
        List<String> authPrefs = new ArrayList<String>(2);
        authPrefs.add(AuthPolicy.DIGEST);
        authPrefs.add(AuthPolicy.BASIC);
        client.getParams().setParameter(AuthPolicy.AUTH_SCHEME_PRIORITY,
                authPrefs);
        GetMethod getMethod = new GetMethod("your_url");
        getMethod.setRequestHeader("Accept", "application/xml");
        client.executeMethod(getMethod);
        int status = getMethod.getStatusCode();
        getMethod.setDoAuthentication(true);
        System.out.println("status: " + status);
        if (status == HttpStatus.SC_OK) {
            String responseBody = getMethod.getResponseBodyAsString();
            String resp = responseBody.replaceAll("\n", " ");
            System.out.println("RESPONSE \n" + resp);
        }
    } catch (Exception e) {
        e.printStackTrace();
    }

答案是,HttpUrlConnection不支持摘要。

因此您必须自己实施RFC2617

您可以使用以下代码作为基线实施:HTTP Digest Auth for Android.

涉及的步骤(参考RFC2617):

  • 如果您收到 401 响应,遍历所有 WWW-Authenticate header 并解析它们:
    • 检查算法是否为 MD5 或未定义,(可选 select auth qop 选项),否则忽略挑战并转到下一个 header.
    • 使用 Authenticator.requestPasswordAuthentication 获取凭据。
    • 使用用户名、领域和密码计算 H(A1)。
    • 存储规范根 URL、领域、HA1、用户名、随机数(+可选算法、不透明和客户端 selected qop 选项(如果存在)。
    • 重试请求。
  • 在每个请求中,遍历您拥有 session 由规范根 URL 存储的信息的所有领域:
    • 使用请求方法和路径计算H(A2)。
    • 使用 HA1、随机数(+ 可选的 nc、cnonce、qop)和 HA2 计算 H(A3)。
    • 构建 Authorization header 并将其添加到您的 HttpUrlConnection
  • 实施某种 session 修剪。

通过使用 Authenticator,您可以确保一旦 HttpUrlConnection 原生支持摘要,您的代码就不会再被使用(因为您一开始就不会收到 401) .

这只是关于如何实施它的简要总结,供您了解。

如果您想更进一步,您可能还想实现 SHA256:RFC7616

HttpUrlConnection不支持摘要认证是正确的。如果您的客户端必须使用 Digest 进行身份验证,您有以下几种选择:

  • 编写您自己的 HTTP 摘要实现。如果您知道需要向哪些服务器进行身份验证并且可以忽略不需要的摘要规范部分,那么这可能是一个不错的选择。下面是实现摘要子集的示例:https://gist.github.com/slightfoot/5624590.
  • 使用外部库 bare-bones-digest,它是 Android 的摘要库。您可以使用它来解析 Digest 质询并生成对它们的响应。它支持常见的摘要用例和一些很少使用的用例,可以在 HttpURLConnection.
  • 之上使用
  • 使用OkHttp together with okhttp-digest,这是一个为O​​kHttp添加Http Digest支持的插件。使用 OkHttp 支持 Digest 很容易,只需添加 okhttp-digest 作为身份验证器,您将获得透明的 Http 摘要支持。如果您已经在使用 OkHttp 或可以切换到它,这可能是一个有吸引力的选择。
  • 使用支持 Digest 的 Apache HttpClient。该问题明确指出 HttpClient 不是一个选项,因此我将其包含在内主要是为了完成。 Google 不推荐使用 HttpClient 并已弃用它。

我最终用我自己实现的 HttpUrlConnection 替换了已弃用的 DefaultHttpClient 并且我自己实现了 digest atuhentication,使用 this 作为模板。
最后的代码看起来像这样:

// requestMethod: "GET", "POST", "PUT" etc.
// Headers: A map with the HTTP-Headers for the request
// Data: Body-Data for Post/Put
int statusCode = this.requestImpl(requestMethod, headers, data);
if (statusCode == HttpURLConnection.HTTP_UNAUTHORIZED && hasUserNameAndPassword) {
    String auth = getResponseHeaderField("WWW-Authenticate");
    // Server needs Digest authetication
    if(auth.startsWith("Digest")){
          // Parse the auth Header
          HashMap<String, String> authFields = parseWWWAuthenticateHeader(auth);
          // Generate Auth-Value for request
          String requestAuth = generateDigestAuth(authFields);
          headers.put("Authorization", authStr);
          statusCode = this.requestImpl(requestMethod, headers, data);
    }
}

所以基本上我发出一个请求,如果它 returns 401,我看,如果服务器想要 digest authentication 并且我有用户名和密码。如果是这样,我解析响应的 auth header,其中包含有关身份验证的所有必要信息。
为了解析 auth header,我使用了某种 StateMachine,描述为 here
解析响应 auth header 后,我使用响应中的信息生成请求 auth header:

    String digestAuthStr = null;

    String uri = getURL().getPath();
    String nonce = authFields.get("nonce");
    String realm = authFields.get("realm");
    String qop = authFields.get("qop");
    String algorithm = authFields.get("algorithm");
    String cnonce = generateCNonce();
    String nc = "1";
    String ha1 = toMD5DigestString(concatWithSeparator(":", username, realm, password));
    String ha2 = toMD5DigestString(concatWithSeparator(":", requestMethod, uri));
    String response = null;
    if (!TextUtils.isEmpty(ha1) && !TextUtils.isEmpty(ha2))
        response = toMD5DigestString(concatWithSeparator(":", ha1, nonce, nc, cnonce, qop, ha2));

    if (response != null) {
        StringBuilder sb = new StringBuilder(128);
        sb.append("Digest ");
        sb.append("username").append("=\"").append(username).append("\", ");
        sb.append("realm").append("=\"").append(realm).append("\", ");
        sb.append("nonce").append("=\"").append(nonce).append("\", ");
        sb.append("uri").append("=\"").append(uri).append("\", ");
        sb.append("qop").append("=\"").append(qop).append("\", ");
        sb.append("nc").append("=\"").append(nc).append("\", ");
        sb.append("cnonce").append("=\"").append(cnonce).append("\"");
        sb.append("response").append("=\"").append(response).append("\"");
        sb.append("algorithm").append("=\"").append(algorithm).append("\"");
        digestAuthStr = sb.toString();
    }

要生成 Client-Nonce,我使用以下代码:

private static String generateCNonce() {
    String s = "";
    for (int i = 0; i < 8; i++)
        s += Integer.toHexString(new Random().nextInt(16));
    return s;
}

我希望这对某人有所帮助。如果代码包含任何错误,请告诉我,以便我进行修复。但现在它似乎起作用了。

对于 Android,我发现 bare-bones-digest 库运行良好:https://github.com/al-broco/bare-bones-digest

  1. 在build.gradle
  2. 中添加一行
  3. 使用上面的示例代码url

有效!