【问题标题】:Retrofit POST request w/ Digest HTTP Authentication: “Cannot retry streamed HTTP body”使用摘要 HTTP 身份验证改造 POST 请求:“无法重试流式 HTTP 正文”
【发布时间】:2014-12-24 04:31:06
【问题描述】:

我正在尝试使用 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 标头,但这似乎很麻烦。

有没有办法解决这个问题?

【问题讨论】:

    标签: android authentication retrofit okhttp


    【解决方案1】:

    作为一种解决方法,我最终将 OkHttp 换成了 Apache 的 HttpClient,后者具有内置的摘要式身份验证。提供一个 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 上设置新的客户端实现:

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

    【讨论】:

    • 你能展示你创建httpurirequest的代码吗?您是否实现了该接口中的所有方法?
    • 您好,我正在努力通过改造来实现摘要式身份验证。互联网上关于这方面的信息太少了。我尝试了您的示例,但错过了“然后使用 delegate.execute(uriRequest)` 执行请求”部分。你能解释一下吗?谢谢
    • 所以delegate 指的是顶部的CloseableHttpClient 私有最终变量。我们正在使用该 HttpClient 来执行 wrappedRequest 对象。希望对您有所帮助!
    • CloseableHttpResponse 执行wrappedRequest 和接口要求的retroFit Response 对象返回之间是不是还存在脱节?
    • 是的,您将构造一个新的retrofit.client.Response 对象,该对象从CloseableHttpClient.execute() 返回的响应中提取其字段。有关示例,请参阅此 gist
    【解决方案2】:

    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"))

    【讨论】:

      【解决方案3】:

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

      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 之后,我再次处理请求,它对我来说工作正常。

      【讨论】:

        猜你喜欢
        • 2014-02-17
        • 1970-01-01
        • 1970-01-01
        • 2023-03-06
        • 2011-05-07
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多