需要帮助创建Android的摘要认证

4
我已经有了下面这段代码:
private class DownloadWebPageTask extends AsyncTask<String, Void, String> 
{
        @Override
        protected String doInBackground(String... theParams) 
        {
            String myUrl = theParams[0];
            String myEmail = theParams[1];
            String myPassword = theParams[2];

            HttpPost post = new HttpPost(myUrl);
            post.addHeader("Authorization","Basic "+ Base64.encodeToString((myEmail+":"+myPassword).getBytes(), 0 ));
            ResponseHandler<String> responseHandler = new BasicResponseHandler();

            String response = null;

            try 
            {
                    response = client.execute(post, responseHandler);
                InputStream content = execute.getEntity().getContent();

                BufferedReader buffer = new BufferedReader(
                            new InputStreamReader(content));
                    String s = "";
                    while ((s = buffer.readLine()) != null) 
                    {
                        response += s;
                    }
                } 
                catch (Exception e) 
                {
                    e.printStackTrace();
                }

            return response;
        }


        @Override
        protected void onPostExecute(String result) 
        {
            }

}

这段代码无法编译,因为我在以下代码处遇到了困惑:

                response = client.execute(post, responseHandler);
                InputStream content = execute.getEntity().getContent();

我从各种示例中摆弄代码得到了这个,不确定客户端应该是什么对象,第一行是否只会让我获得服务器响应,或者我必须获取InputStream并读取服务器响应?请帮助我正确理解如何做。谢谢!

  1. 客户端是HttpClient。
  2. client.execute(...) 返回的是HttpResponse,而不是String。
  3. InputStream content = response.getEntity().getContent()(response指的是HttpResponse)。
- Vyacheslav Shylkin
@appserv 我有点困惑客户端对象是什么 :) 我该如何获取客户端对象? :) - GeekedOut
HttpClient client = new DefaultHttpClient(); - Vyacheslav Shylkin
3个回答

5
我已经成功使用OkHttp进行摘要认证。在此代码示例中,我还使用了Dagger和Robospice-retrofit。我的做法是创建一个OkHttp Authenticator并将其分配给我的自定义OkHttp客户端。
认证器类实现了一个authenticate方法,每当服务器遇到401错误并期望返回Authorization头时调用该方法(如果它期望Proxy-Authorization,则应实现authenticateProxy方法)。
它的基本作用是包装对HttpClient DigestScheme的调用,并使其可用于OkHttp。目前它不会增加nc计数器。这可能会导致您的服务器出现问题,因为它可能被解释为重放攻击。
public class DigestAuthenticator implements com.squareup.okhttp.Authenticator {
    @Inject DigestScheme mDigestScheme;
    @Inject org.apache.http.auth.Credentials mCredentials;

    @Override
    public Request authenticate(Proxy proxy, Response response) throws IOException {
        String authHeader = buildAuthorizationHeader(response);
        if (authHeader == null) {
            return null;
        }
        return response.request().newBuilder().addHeader("Authorization", authHeader).build();
    }

    @Override
    public Request authenticateProxy(Proxy proxy, Response response) throws IOException {
        return null;
    }

    private String buildAuthorizationHeader(Response response) throws IOException {
        processChallenge("WWW-Authenticate", response.header("WWW-Authenticate"));
        return generateDigestHeader(response);
    }

    private void processChallenge(String headerName, String headerValue) {
        try {
            mDigestScheme.processChallenge(new BasicHeader(headerName, headerValue));
        } catch (MalformedChallengeException e) {
            Timber.e(e, "Error processing header " + headerName + " for DIGEST authentication.");
        }
    }

    private String generateDigestHeader(Response response) throws IOException {
        org.apache.http.HttpRequest request = new BasicHttpRequest(
                response.request().method(),
                response.request().uri().toString()
        );

        try {
            return mDigestScheme.authenticate(mCredentials, request).getValue();
        } catch (AuthenticationException e) {
            Timber.e(e, "Error generating DIGEST auth header.");
            return null;
        }
    }
}

接下来,身份验证器将在使用提供程序构建的OkHttpClient中使用:

public class CustomClientProvider implements Client.Provider {
    @Inject DigestAuthenticator mDigestAuthenticator;

    @Override
    public Client get() {
        OkHttpClient client = new OkHttpClient();
        client.setAuthenticator(mDigestAuthenticator);
        return new OkClient(client);
    }
}

最后,在createRestAdapterBuilder函数中,客户端被设置为RetrofitRobospice服务器。

public class ApiRetrofitSpiceService extends RetrofitJackson2SpiceService {
    @Inject Client.Provider mClientProvider;

    @Override
    public void onCreate() {
        App.get(this).inject(this);
        super.onCreate();
        addRetrofitInterface(NotificationRestInterface.class);
    }

    @Override
    protected String getServerUrl() {
        return Constants.Url.BASE;
    }

    @Override
    protected RestAdapter.Builder createRestAdapterBuilder() {
        return super.createRestAdapterBuilder()
                .setClient(mClientProvider.get());
    }
}

奇怪,我总是使用你的技术得到400错误请求 - 我没有使用RetrofitRobospice - 你认为这是否相关?我不知道它如何相关,但我对robospice没有太多经验。 - k_wisniewski
你还在使用 Retrofit 吗?能否发布完整的请求和响应内容? - Félix Queiruga
是的,我使用 Retrofit。在 authenticate 的返回语句中生成的请求只有 2 个标头,它们在这里(以及响应):http://pastebin.com/QV8hm8Kq - k_wisniewski
请求是GET或POST。我无法确定您的请求有什么问题。您可以查看HTTP摘要认证的RFC规范,以确保请求已正确生成。您还可以检查服务器是否已正确配置。如果您找到了问题,请告诉我们,我会更新我的答案。 - Félix Queiruga
这是GET请求。服务器似乎没问题,因为它能正确响应使用CURL发出的请求。 - k_wisniewski

3

Android所附带的Apache的HttpClient版本基于旧版、BETA之前的HttpClient。Google已经长期建议不要使用它在Android 6.0中将其删除。Google的替代品HttpURLConnection不支持HTTP摘要认证, 只支持基本认证。

这让您有几个选择,包括:

  • 迁移到 HttpURLConnection(如Google建议),并使用 bare-bones-digest 库进行摘要认证。以下是示例。
  • 使用 OkHttp 库替代 HttpURLConnectionHttpClient。OkHttp 不支持默认的摘要认证,但有一个 okhttp-digest 库实现了摘要认证器。以下是示例。
  • 继续使用(已弃用的)HttpClient,通过在构建中显式添加 'org.apache.http.legacy' 库来解决。这在 Android 6.0 的更改列表 中已经提到。
  • 有一个Apache项目专门为将较新版本的 HttpClient 移植到 Android,但该项目已停止维护。请查看Apache关于 HttpClient for Android 的页面
  • 自行实现HTTP摘要认证。
这是一个详细的示例,展示如何使用bare-bones-digestHttpURLConnection进行请求认证(从项目的github页面复制):
// Step 1. Create the connection
URL url = new URL("http://httpbin.org/digest-auth/auth/user/passwd");
HttpURLConnection connection = (HttpURLConnection) url.openConnection();

// Step 2. Make the request and check to see if the response contains
// an authorization challenge
if (connection.getResponseCode() == HttpURLConnection.HTTP_UNAUTHORIZED) {
    // Step 3. Create a authentication object from the challenge...
    DigestAuthentication auth = DigestAuthentication.fromResponse(connection);
    // ...with correct credentials
    auth.username("user").password("passwd");

    // Step 4 (Optional). Check if the challenge was a digest
    // challenge of a supported type
    if (!auth.canRespond()) {
        // No digest challenge or a challenge of an unsupported
        // type - do something else or fail
        return;
    }

    // Step 5. Create a new connection, identical to the original
    // one..
    connection = (HttpURLConnection) url.openConnection();
    // ...and set the Authorization header on the request, with the
    // challenge response
    connection.setRequestProperty(
        DigestChallengeResponse.HTTP_HEADER_AUTHORIZATION,
        auth.getAuthorizationForRequest("GET", connection.getURL().getPath()));
}

这里是一个使用OkHttpokhttp-digest的例子(从okhttp-digest页面复制):

client = new OkHttpClient();
final DigestAuthenticator authenticator = new DigestAuthenticator(new Credentials("username", "pass"));

final Map<String, CachingAuthenticator> authCache = new ConcurrentHashMap<>();
client.interceptors().add(new AuthenticationCacheInterceptor(authCache));
client.setAuthenticator(new CachingAuthenticatorDecorator(authenticator, authCache));

Request request = new Request.Builder()
  .url(url);
  .get()
  .build();
Response response = client.newCall(request).execute();

3
您可能想要转换为HttpURLConnection。根据本文章,它的API比HttpClient简单,并且在Android上得到更好的支持。如果您选择使用HttpURLConnection进行认证,则非常简单:
Authenticator.setDefault(new Authenticator() {
    @Override
    protected PasswordAuthentication getPasswordAuthentication() {
        return new PasswordAuthentication("username", "password".toCharArray());
    }
});

接下来,继续像往常一样使用 HttpURLConnection。以下是一个简单的示例:

final URL url = new URL("http://example.com/");
final HttpURLConnection conn = (HttpURLConnection) url.openConnection();
final InputStream is = conn.getInputStream();
final byte[] buffer = new byte[8196];
int readCount;
final StringBuilder builder = new StringBuilder();
while ((readCount = is.read(buffer)) > -1) {
    builder.append(new String(buffer, 0, readCount));
}
final String response = builder.toString();

使用HttpUrlConnection有什么好处?此外,您建议的方法是否仍符合Android摘要认证方法?谢谢! - GeekedOut
@GeekedOut 切换到 HttpURLConnection 的好处在链接的文章中有详细说明。至于摘要认证支持,是的,Authenticator 方法支持 HTTP 基本认证、HTTP 摘要认证、NTLM 和 SPNEGO 认证,如此处所述(http://docs.oracle.com/javase/7/docs/technotes/guides/net/http-auth.html)。它会根据服务器的响应选择适当的认证方法。 - Felix
但是您提供的链接讨论了其他问题。我很困惑应该在哪里放置并调用 Authenticator 代码片段。它应该放在扩展 AsyncTask 的内部类中吗?还是外部?它是隐式调用还是显式调用?谢谢! - GeekedOut
我提供的链接讨论了HttpURLConnection和Apache的HttpClient,以及前者在Android上的优越性。至于代码,我不知道你所说的“隐式或显式”的意思。为了简单起见,您可以将我的答案中的两个代码片段合并到AsyncTask中的一个大的doInBackground方法中。然而,既然你问了“这段代码放在哪里”的问题,我会建议你学习更多的Java知识,因为这不是一个特定于Android的问题。 - Felix
@Felix - 你提到的链接文章适用于NTLM和SPNEGO等协议,你有成功将它们应用到实际中吗?我已经实现了Basic认证方式,但是对于NTLM/SPNEGO认证方式,getPasswordAuthentication()从未被调用。你需要额外执行一些步骤吗? - Vino
显示剩余5条评论

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