在Android中使用HttpURLConnection进行摘要认证

6
如题所述,我正在尝试在Android中进行摘要身份验证。
到目前为止,我一直使用的是DefaultHttpClient及其身份验证方法(使用UsernamePasswordCredentials等),但自Android 5以来已被弃用,并将在Android 6中删除。
因此,我打算从DefaultHttpClient切换到HttpUrlConnection
现在我正在尝试实现摘要身份验证,这应该很简单,如这里所解释的那样。
Authenticator.setDefault(new Authenticator() {
    protected PasswordAuthentication getPasswordAuthentication() {
        return new PasswordAuthentication(username, password);
    }
});

但是由于某种原因,getPasswordAuthentication从未被调用。
在寻找此问题的过程中,我发现了不同的帖子,称Android不支持HttpUrlConnection中的摘要认证,但这些帖子来自2010-2012年,所以我不确定这是否仍然正确。此外,我们在桌面Java应用程序中使用具有摘要认证的HttpUrlConnection,在那里它确实有效。

我还发现了一些关于OkHttp的帖子。 Android似乎在幕后使用OkHttp(更具体地说是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"

所以基本身份验证不起作用,因为服务器正在请求摘要。
5个回答

7
答案是,HttpUrlConnection不支持摘要认证。因此,您需要自己实现RFC2617。您可以使用以下代码作为基线实现:Android的HTTP Digest Auth
步骤如下(参见RFC2617的参考):
- 如果您收到401响应,请迭代所有WWW-Authenticate标头并解析它们: - 检查算法是否为MD5或未定义(可选择auth qop选项),否则请忽略挑战并转到下一个标头。 - 使用Authenticator.requestPasswordAuthentication获取凭据。 - 使用用户名、领域和密码计算H(A1)。 - 存储规范根URL、领域、HA1、用户名、nonce(+可选算法、opaque和客户端选择的qop选项,如果存在)。 - 重试请求。 - 在每个请求上,迭代您存储的所有领域,以规范根URL为准: - 使用请求方法和路径计算H(A2)。 - 使用HA1、nonce(+可选nc、cnonce、qop)和HA2计算H(A3)。 - 构建并添加Authorization标头到您的HttpUrlConnection。 - 实现某种形式的会话修剪。
通过使用Authenticator,您可以确保一旦HttpUrlConnection本地支持摘要,您的代码将不再使用(因为您不会首先收到401)。这只是一个快速概述,让您了解如何实现它。
如果您想进一步了解,您可能还想实现SHA256:RFC7616

谢谢你的回答。仅仅是Android版本的HttpUrlConnection不支持摘要认证,还是默认的java.net.HttpURLConnection也不支持呢? - Robert P
@Springrbua 只有在 Android 上不支持,至少据我所知。也许你可以找到 JDK 实现的来源。 - Nappy
好的,谢谢你的回答。我现在会继续使用DefaultHttpClient,但似乎迟早需要切换,所以我将不得不自己实现摘要。谢谢! - Robert P

5

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

  • 编写自己的HTTP Digest实现。如果您知道需要与哪些服务器进行身份验证,并且可以忽略不需要的摘要规范部分,则这可能是一个很好的选择。下面是一个实现了部分摘要的示例:https://gist.github.com/slightfoot/5624590
  • 使用外部库bare-bones-digest,这是一个针对Android的摘要库。您可以使用它来解析摘要挑战并生成对其的响应。它支持常见的摘要用例和一些很少使用的用例,并可在HttpURLConnection上使用。
  • 结合OkHttpokhttp-digest使用,后者是一个插件,可将Http摘要支持添加到OkHttp中。使用OkHttp支持Digest很容易,只需将okhttp-digest作为验证器添加即可,就可以透明地支持Http摘要。如果您已经使用OkHttp或愿意转换到它,则这可能是一个吸引人的选择。
  • 使用支持Digest的Apache HttpClient。问题明确指出HttpClient不是一个选项,所以我主要为了完整性而包括它。Google不建议使用HttpClient并已将其弃用。

我几个月前用 HttpUrlConnection 替换了 DefaultHttpClient,并且我自己实现了 Digest authentication,使用 (这里)[https://gist.github.com/slightfoot/5624590] 作为模板。我可以将我的代码添加为答案,这可能会帮助其他人! - Robert P

2

您是否尝试手动设置标题,例如:

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

此外,请注意 Jellybeans 中的一些问题和在尝试执行 POST 请求时出现的错误:HTTP Basic Authentication issue on Android Jelly Bean 4.1 using HttpURLConnection
编辑:对于摘要身份验证,请参考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();
    }

1
我编辑了我的问题并添加了服务器提供的WWW-Authenticate头。它说“digest”,所以我猜它期望和需要摘要认证... - Robert P
这实际上是我现在正在使用的(Apache HttpClient),它可以工作,但在Android 5中已被弃用,并且似乎在Android 6中已被删除,因此我想按建议将其替换为HttpUrlConnection... - Robert P
你是否也检查了那个问题的其他答案,有很多种尝试的方法。 - Bas van Stein
链接问题的所有3个答案似乎都差不多,都使用了DigestScheme,它是org.apache.hppt的一部分。实际上,这个包已经被弃用并将被删除。所以,如果我想支持Android 6,就不能使用任何来自org.apache.http.*包的类... - Robert P
不,我还没有在Android 6上测试过它,但我正在使用Android 5.1.1平板电脑进行开发,目标API也是Android 5.1.1(API Level 22),因为在那里已经过时了。因此,在那里,它必须以某种方式与HttpUrlConnection一起工作。我的意思是,如果没有替代方法,你就不能废弃某个东西,对吧?请问你有任何信息来源称它可以在Android 6中使用吗? - Robert P
显示剩余8条评论

1

1

我最终用我自己实现的HttpUrlConnection替换了已弃用的DefaultHttpClient,并且使用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);
    }
}

基本上,我发送一个请求,如果返回401,则查看服务器是否需要摘要身份验证以及我是否有用户名和密码。如果是这种情况,我解析响应的auth头,其中包含有关身份验证的所有必要信息。
为了解析auth头,我使用某种状态机,该状态机在此处描述
解析响应auth头后,我使用响应中的信息生成请求auth头:
    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();
    }

为了生成客户端随机数,我正在使用以下代码:
private static String generateCNonce() {
    String s = "";
    for (int i = 0; i < 8; i++)
        s += Integer.toHexString(new Random().nextInt(16));
    return s;
}

我希望这能对某人有所帮助。如果代码中有任何错误,请告诉我,以便我可以修复它。但现在看起来它似乎可以工作。


这是一个非常紧凑和高效的摘要实现,但需要注意的是它只实现了摘要的一小部分。如果你只与一个特定的服务器一起工作并可以进行测试,那么这很好。如果你需要与许多不同的服务器兼容,并处理像多个挑战、在引号字符串中转义字符、SHA-256摘要、opaque指令、重用挑战等问题,那么最好使用其中一个库(裸骨摘要、okhtt摘要、Apache HttpClient等)来覆盖边缘情况。 - user829876
@user829876 我完全同意你的观点。如果你需要完整支持摘要,那么你应该选择一个已知且经过测试的库。 - Robert P

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接