Retrofit之Invocation

目前Android开发接口请求流行使用 Retrofit+rxjava+okhttp, 绝大多数的请求也都可以很轻松的实现或者有现成的demo可以参考, 也有个别特殊情况.

需求

  • http 头部加字段:
APP-PARAMS = version+||+client+||+channel+||+device+||+timestamp
参数名 类型 说明
version string 版本号
client int 客户端(安卓,ios)
channel int 渠道
device string 设备号
timestamp int 时间戳

以上基本字段全部参与加密签名。

  • 签名规则

除了key(密钥,由服务端提供)以外,其他参数(含post和get)按照ascii的顺序排序,各个参数值以“||”拼接成字符串后再追加key,之后再用md5加密生成签名。

encryptString = paramA + '||' + paramB + '||' + key
sign = MD5(encryptString);

出于安全和防刷和其他目的, 服务端做出上面的请求规则, 可能有更好的方式来做, 不在讨论范围, 这里只针对需求来实现. 除了上面的基本字段需要加密外, 不同的请求有不同的加密字段, 可能额外需要5个字段但只有2个需要加密, 加密签名后的字段最终放入sign字段发送请求.

参数名 必选 类型 说明 加密
type int 类型(1-登录,2-重置密码)
xxx string 其他参数
sign string 签名

分析

​ 请求签名这种属于通用性的规则, 首选的做法就是使用okhttp的拦截器对每个请求进行签名加密. 从上面的需求可以看到请求需要传一个Header, 请求需要一个签名后的sign字段, Header和sign字段中需要使用到一些共同的字段(基本字段和接口中标注需要加密的, 时间戳本地获取要确保一样), 哪些字段需要签名都要可以随接口来自定义的, 于是想到了注解. 自定义注解Sign作用于retrofit请求的参数表示需要签名:

public interface Server{
  @GET("/server/a")
  Observable request(@Sign @Query("type") int type, @Query("xxx") String xxx);
}

上面定义的请求接口可以清晰的看出需要参与签名的字段, 变更时也可以灵活修改。 由于sign字段所有请求都需要,做统一处理。

​ 接下来要处理的就是利用接口中的参数生成Header和sign配置到请求中去。而注解的方式的实现,需要能在设置请求时将java方法和参数都获取到。 所以如果在拦截器中处理,那么拦截器需要获取到调用的方法和参数。在Retrofit v2.4.0及之前的版本都不能在拦截器中获取到任何方法的调用信息,需要对源码做一定的修改。

Retrofit源码修改

这里针对v2.4.0版本做修改 retrofit-2.4.0

查找Request生成的代码

在ServiceMethod.toCall 方法中

  /** Builds an HTTP request from method arguments. */
  okhttp3.Call toCall(@Nullable Object... args) throws IOException {
    RequestBuilder requestBuilder = new RequestBuilder(httpMethod, baseUrl, relativeUrl, headers, contentType, hasBody, isFormEncoded, isMultipart);
    ParameterHandler[] handlers = (ParameterHandler[]) parameterHandlers;

    int argumentCount = args != null ? args.length : 0;
    if (argumentCount != handlers.length) {
      throw new IllegalArgumentException("Argument count (" + argumentCount
          + ") doesn't match expected count (" + handlers.length + ")");
    }

    for (int p = 0; p < argumentCount; p++) {
      handlers[p].apply(requestBuilder, args[p]);
    }

    return callFactory.newCall(requestBuilder.build());
  }

添加接口

ServiceMethod.toCall 这个方法中虽然生成了请求的Request对象, 但是只传了方法调用的参数进来, 并没有方法提供给我们处理签名字段。看源码的话其实ServiceMethod.Builder 中是有原始的java方法的.

final class ServiceMethod {
  static final class Builder {
    final Retrofit retrofit;
    final Method method;
  }
}

添加一个Method字段到ServiceMethod, toCall就可以获取到原始方法了。然后笔者将toCall的return语句修改如下:

Request request = requestBuilder.build();
if (callParamsInjector != null) {
  request = callParamsInjector.onInject(request, mJavaMethod, args);
}
return callFactory.newCall(request);

设计接口:

public interface CallParamsInjector {
    /**
     * Call {@link Request} creating, inject something to {@link Request}.
     */
    Request onInject(Request request, Method method, Object... args);
}

添加Retrofit.Builder.parameterInjector(CallParamsInjector), ServiceMethod中的callParamsInjector从Retrofit对象中来(查看Retrofit源码,这里简单描述)。

进行签名

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface Sign {
  // 为不同类型的参数提供转换, 如float只要两位精度
  SignConverter value() default SignConverter.TOSTRING;

  enum Converter {
    TOSTRING {
      @Override
      String apply(Object value) {
        return String.valueOf(value);
      }
    },
    Float2 {
      @Override
      String apply(Object value) {
        if (value instanceof Float)
          return String.format("%.2f", value);
        return TOSTRING.apply(value);
      }
    };

    abstract String apply(Object value);
  }
}

// 该注解标注的方法, 所有参数进行签名
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface SignAll {
}

public class SignInjector implements CallParamsInjector {

  @Override
  public Request onInject(Request request, Method method, Object... args) {
    ArrayList signParams = parseAnnotations(method, args);
    String[] params = SignUtil.defaultParams();
    Collections.addAll(signParams, params);
    String sign = SignUtil.sign(signParams);
    HttpUrl httpUrl = request.url().newBuilder()
      .addQueryParameter("sign", sign)
      .build();
    return request.newBuilder().url(httpUrl)
      .addHeader("APP-PARAMS", SignUtil.genHeader(params))
      .build();
  }

  private ArrayList parseAnnotations(Method method, Object[] args) {
    ArrayList signParams = new ArrayList<>();
    if(method.getAnnotation(SignAll.class) != null){
      for(Object arg : args)
        signParams.add(String.valueOf(arg));
      return signParams;
    }

    // not null
    Annotation[][] parameterAnnotations = method.getParameterAnnotations();
    loop: 
    for (int i = 0; i < parameterAnnotations.length; i++) {
      if (parameterAnnotations[i].length == 1) {
        continue;
      }
      for (Annotation annotation : parameterAnnotations[i]) {
        if (annotation instanceof Sign) {
          Sign.Converter converter = ((Sign) annotation).value();
          signParams.add(converter.apply(args.get(i)));
          continue loop;//抑制多个@Sign
        }
      }
    }
    return signParams;
  }
}

将SignInjector对象设置到Retrofit.Builder中即可。

注意

对retrofit修改后, gson-converter, adapter-rxjava2等也不要使用远程仓库的, 不然依赖可能会有问题

Retrofit 2.5.0

当准备升级Retrofit到2.5.0时,发现2.5.0做了不小的改动,找到创建Request的方法

okhttp3.Request create(Object[] args) throws IOException {
  //...
  return requestBuilder.get()
    .tag(Invocation.class, new Invocation(method, argumentList))
    .build();
}

RequestFactory.create#L92 他将接口调用的方法和参数列表包裹到Invocation 对象中,放在了okhttp3.Request.Builder 的tag里了。签名的首选也是拦截器,限于2.4.0前无法获得方法和参数列表。而使用Retrofit 2.5.0只需要在拦截器中获取到Invocation 对象,后续的签名如上文所述。然而这个功能似乎没有现在其change log 中写出来。

小结

Retrofit是一个非常好的开源网络请求框架,非常值得研究。笔者为求实现而修改的拙劣代码在优雅的Request.Builder.tag 面前不值一提。谨以此短文来记录与大神的差距。

你可能感兴趣的:(Retrofit之Invocation)