使用摘要HTTP身份验证更改POST请求:“无法重试流式HTTP正文”

时间:2014-10-28 16:49:28

标签: android authentication retrofit okhttp

我正在尝试使用Retrofit实现摘要式身份验证。我的第一个解决方案在OkHttpClient上设置了OkHttp的Authenticator的实现:

class MyAuthenticator implements Authenticator {
  private final DigestScheme digestScheme = new DigestScheme();
  private final Credentials credentials = new UsernamePasswordCredentials("user", "pass");

  @Override public Request authenticate(Proxy proxy, Response response) throws IOException {
    try {
      digestScheme.processChallenge(new BasicHeader("WWW-Authenticate", response.header("WWW-Authenticate")));
      HttpRequest request = new BasicHttpRequest(response.request().method(), response.request().uri().toString());
      String authHeader = digestScheme.authenticate(credentials, request).getValue();
      return response.request().newBuilder()
          .addHeader("Authorization", authHeader)
          .build();
    } catch (Exception e) {
      throw new AssertionError(e);
    }
  }

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

这适用于通过Retrofit进行GET请求。但是,如this StackOverflow question中所述,POST请求会导致“无法重试流式HTTP正文”异常:

Caused by: java.net.HttpRetryException: Cannot retry streamed HTTP body
        at com.squareup.okhttp.internal.http.HttpURLConnectionImpl.getResponse(HttpURLConnectionImpl.java:324)
        at com.squareup.okhttp.internal.http.HttpURLConnectionImpl.getResponseCode(HttpURLConnectionImpl.java:508)
        at com.squareup.okhttp.internal.http.HttpsURLConnectionImpl.getResponseCode(HttpsURLConnectionImpl.java:136)
        at retrofit.client.UrlConnectionClient.readResponse(UrlConnectionClient.java:94)
        at retrofit.client.UrlConnectionClient.execute(UrlConnectionClient.java:49)
        at retrofit.RestAdapter$RestHandler.invokeRequest(RestAdapter.java:357)
        at retrofit.RestAdapter$RestHandler.invoke(RestAdapter.java:282)
        at $Proxy3.login(Native Method)
        at com.audax.paths.job.LoginJob.onRunInBackground(LoginJob.java:41)
        at com.audax.library.job.AXJob.onRun(AXJob.java:25)
        at com.path.android.jobqueue.BaseJob.safeRun(BaseJob.java:108)
        at com.path.android.jobqueue.JobHolder.safeRun(JobHolder.java:60)
        at com.path.android.jobqueue.executor.JobConsumerExecutor$JobConsumer.run(JobConsumerExecutor.java:172)
        at java.lang.Thread.run(Thread.java:841)

Jesse Wilson解释说我们无法在身份验证后重新发送我们的请求,因为POST主体已被抛弃。但是由于摘要式身份验证,我们需要返回的WWW-Authenticate标头,因此我们无法使用RequestInterceptor来添加标头。也许可以在RequestInterceptor中执行单独的HTTP请求,并在响应中使用WWW-Authenticate标头,但这看起来很糟糕。

有什么方法吗?

3 个答案:

答案 0 :(得分:1)

作为一种解决方法,我最终使用Apache的HttpClient交换了OkHttp,后者具有内置的摘要式身份验证。提供retrofit.client.Client的实现,该实现将其请求委托给Apache的HttpClient:

import retrofit.client.Client;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.auth.Credentials;
import org.apache.http.auth.AuthScope;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.HttpClientBuilder;
import retrofit.client.Request;
import retrofit.client.Response;

public class MyClient implements Client {
  private final CloseableHttpClient delegate;

  public MyClient(String user, String pass, String hostname, String scope) {
    Credentials credentials = new UsernamePasswordCredentials(user, pass);
    AuthScope authScope = new AuthScope(hostname, 443, scope);

    CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
    credentialsProvider.setCredentials(authScope, credentials);

    delegate = HttpClientBuilder.create()
        .setDefaultCredentialsProvider(credentialsProvider)
        .build();
  }

  @Override public Response execute(Request request) {
    //
    // We're getting a Retrofit request, but we need to execute an Apache
    // HttpUriRequest instead. Use the info in the Retrofit request to create
    // an Apache HttpUriRequest.
    //
    String method = req.getMethod();

    ByteArrayOutputStream bos = new ByteArrayOutputStream();
    if (request.getBody() != null) {
      request.getBody().writeTo(bos);
    }
    String body = new String(bos.toByteArray());

    HttpUriRequest wrappedRequest;
    switch (method) {
      case "GET":
        wrappedRequest = new HttpGet(request.getUrl());
        break;
      case "POST":
        wrappedRequest = new HttpPost(request.getUrl());
        wrappedRequest.addHeader("Content-Type", "application/xml");
        ((HttpPost) wrappedRequest).setEntity(new StringEntity(body));
        break;
      case "PUT":
        wrappedRequest = new HttpPut(request.getUrl());
        wrappedRequest.addHeader("Content-Type", "application/xml");
        ((HttpPut) wrappedRequest).setEntity(new StringEntity(body));
        break;
      case "DELETE":
        wrappedRequest = new HttpDelete(request.getUrl());
        break;
      default:
        throw new AssertionError("HTTP operation not supported.");
    }

    //
    // Then execute the request with `delegate.execute(uriRequest)`.
    //
    // ...
    //
  }

然后在RestAdapter.Builder上设置新的Client实现:

RestAdapter restAdapter = new RestAdapter.Builder()
    .setClient(new MyClient("jason", "pass", "something.com", "Some Scope"))
    .setEndpoint("https://something.com/api")
    .build();

答案 1 :(得分:1)

Retofit ApacheClient 实现从https://github.com/square/retrofit/blob/master/retrofit/src/main/java/retrofit/client/ApacheClient.java复制到您自己的源代码,将其重命名为MyClient并添加此构造函数:

public MyClient(String user, String pass) {
    this();

    DefaultHttpClient defaultHttpClient = (DefaultHttpClient)this.client;

    Credentials credentials = new UsernamePasswordCredentials(user, pass);
    AuthScope authScope = new AuthScope(AuthScope.ANY_HOST, AuthScope.ANY_PORT, AuthScope.ANY_REALM);

    CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
    credentialsProvider.setCredentials(authScope, credentials);

    defaultHttpClient.setCredentialsProvider(credentialsProvider);
}

现在,您可以在 RestAdapter 构建器上使用 MyClient .setClient(new MyClient("user", "pass"))

答案 2 :(得分:0)

我使用带有Interceptor的OkHttp客户端解决了这个问题,我曾经尝试过三次来执行请求。我在下面创建了拦截器:

import android.content.Context;
import android.util.Log;

import com.crmall.androidcommon.helpers.CacheManagerHelper;
import com.crmall.maisequipe.helper.ApplicationSystemHelper;
import com.squareup.okhttp.Interceptor;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.Response;

import org.apache.http.HttpRequest;
import org.apache.http.auth.AuthenticationException;
import org.apache.http.auth.Credentials;
import org.apache.http.auth.MalformedChallengeException;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.impl.auth.DigestScheme;
import org.apache.http.message.BasicHeader;
import org.apache.http.message.BasicHttpRequest;

import java.io.IOException;
import java.net.HttpURLConnection;

/**
 * Interceptor used to authorize requests.
 */
public class AuthorizationInterceptor implements Interceptor {

    private final DigestScheme digestScheme = new DigestScheme();
    private final Credentials credentials;

    public AuthorizationInterceptor(String username, String password) throws IOException, ClassNotFoundException {
        credentials = new UsernamePasswordCredentials(username, password);
    }

    @Override
    public Response intercept(Chain chain) throws IOException {
        Request request = chain.request();

        Response response = chain.proceed(request);

        if (response.code() == HttpURLConnection.HTTP_UNAUTHORIZED) {

            String authHeader = buildAuthorizationHeader(response);
            if (authHeader != null) {
                request = request.newBuilder().addHeader("Authorization", authHeader).build();

                response = chain.proceed(request);

                if (response.code() == HttpURLConnection.HTTP_BAD_REQUEST || response.code() == HttpURLConnection.HTTP_UNAUTHORIZED) {
                    response = chain.proceed(request);
                }
            }
        }

        return response;

    }

    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 {
            digestScheme.processChallenge(new BasicHeader(headerName, headerValue));
        } catch (MalformedChallengeException e) {
            Log.e("AuthInterceptor", "Error processing header " + headerName + " for DIGEST authentication.");
            e.printStackTrace();
        }
    }

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

        try {
            return digestScheme.authenticate(credentials, request).getValue();
        } catch (AuthenticationException e) {
            Log.e("AuthInterceptor", "Error generating DIGEST auth header.");
            e.printStackTrace();
            return null;
        }
    }
}

有时在第二次请求OkHttp客户端返回响应状态代码400或401之后,我再次处理请求并且它对我来说工作正常。