目前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
添加接口
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
面前不值一提。谨以此短文来记录与大神的差距。