目前Android開(kāi)發(fā)接口請(qǐng)求流行使用 Retrofit+rxjava+okhttp, 絕大多數(shù)的請(qǐng)求也都可以很輕松的實(shí)現(xiàn)或者有現(xiàn)成的demo可以參考, 也有個(gè)別特殊情況.
需求
- http 頭部加字段:
APP-PARAMS = version+||+client+||+channel+||+device+||+timestamp
參數(shù)名 | 類型 | 說(shuō)明 |
---|---|---|
version | string | 版本號(hào) |
client | int | 客戶端(安卓,ios) |
channel | int | 渠道 |
device | string | 設(shè)備號(hào) |
timestamp | int | 時(shí)間戳 |
以上基本字段全部參與加密簽名瑰抵。
- 簽名規(guī)則
除了key(密鑰乓搬,由服務(wù)端提供)以外,其他參數(shù)(含post和get)按照ascii的順序排序茴迁,各個(gè)參數(shù)值以“||”拼接成字符串后再追加key,之后再用md5加密生成簽名酱塔。
encryptString = paramA + '||' + paramB + '||' + key
sign = MD5(encryptString);
出于安全和防刷和其他目的, 服務(wù)端做出上面的請(qǐng)求規(guī)則, 可能有更好的方式來(lái)做, 不在討論范圍, 這里只針對(duì)需求來(lái)實(shí)現(xiàn). 除了上面的基本字段需要加密外, 不同的請(qǐng)求有不同的加密字段, 可能額外需要5個(gè)字段但只有2個(gè)需要加密, 加密簽名后的字段最終放入sign字段發(fā)送請(qǐng)求.
參數(shù)名 必選 類型 說(shuō)明 加密 type 是 int 類型(1-登錄,2-重置密碼) 是 xxx 是 string 其他參數(shù) 否 sign 是 string 簽名 否
分析
? 請(qǐng)求簽名這種屬于通用性的規(guī)則, 首選的做法就是使用okhttp的攔截器對(duì)每個(gè)請(qǐng)求進(jìn)行簽名加密. 從上面的需求可以看到請(qǐng)求需要傳一個(gè)Header, 請(qǐng)求需要一個(gè)簽名后的sign字段, Header和sign字段中需要使用到一些共同的字段(基本字段和接口中標(biāo)注需要加密的, 時(shí)間戳本地獲取要確保一樣), 哪些字段需要簽名都要可以隨接口來(lái)自定義的, 于是想到了注解. 自定義注解Sign作用于retrofit請(qǐng)求的參數(shù)表示需要簽名:
public interface Server{
@GET("/server/a")
Observable<ResponseEntity> request(@Sign @Query("type") int type, @Query("xxx") String xxx);
}
上面定義的請(qǐng)求接口可以清晰的看出需要參與簽名的字段, 變更時(shí)也可以靈活修改绳矩。 由于sign字段所有請(qǐng)求都需要,做統(tǒng)一處理疟呐。
? 接下來(lái)要處理的就是利用接口中的參數(shù)生成Header和sign配置到請(qǐng)求中去脚曾。而注解的方式的實(shí)現(xiàn),需要能在設(shè)置請(qǐng)求時(shí)將java方法和參數(shù)都獲取到启具。 所以如果在攔截器中處理本讥,那么攔截器需要獲取到調(diào)用的方法和參數(shù)。在Retrofit v2.4.0及之前的版本都不能在攔截器中獲取到任何方法的調(diào)用信息鲁冯,需要對(duì)源碼做一定的修改拷沸。
Retrofit源碼修改
這里針對(duì)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<Object>[] handlers = (ParameterHandler<Object>[]) 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
這個(gè)方法中雖然生成了請(qǐng)求的Request對(duì)象, 但是只傳了方法調(diào)用的參數(shù)進(jìn)來(lái), 并沒(méi)有方法提供給我們處理簽名字段∈硌荩看源碼的話其實(shí)ServiceMethod.Builder
中是有原始的java方法的.
final class ServiceMethod<R, T> {
static final class Builder<T, R> {
final Retrofit retrofit;
final Method method;
}
}
添加一個(gè)Method字段到ServiceMethod, toCall就可以獲取到原始方法了撞芍。然后筆者將toCall的return語(yǔ)句修改如下:
Request request = requestBuilder.build();
if (callParamsInjector != null) {
request = callParamsInjector.onInject(request, mJavaMethod, args);
}
return callFactory.newCall(request);
設(shè)計(jì)接口:
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對(duì)象中來(lái)(查看Retrofit源碼,這里簡(jiǎn)單描述)。
進(jìn)行簽名
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface Sign {
// 為不同類型的參數(shù)提供轉(zhuǎn)換, 如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);
}
}
// 該注解標(biāo)注的方法, 所有參數(shù)進(jìn)行簽名
@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<String> 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<String> parseAnnotations(Method method, Object[] args) {
ArrayList<String> 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;//抑制多個(gè)@Sign
}
}
}
return signParams;
}
}
將SignInjector對(duì)象設(shè)置到Retrofit.Builder中即可跨扮。
注意
對(duì)retrofit修改后, gson-converter, adapter-rxjava2等也不要使用遠(yuǎn)程倉(cāng)庫(kù)的, 不然依賴可能會(huì)有問(wèn)題
Retrofit 2.5.0
當(dāng)準(zhǔn)備升級(jí)Retrofit到2.5.0時(shí)序无,發(fā)現(xiàn)2.5.0做了不小的改動(dòng),找到創(chuàng)建Request的方法
okhttp3.Request create(Object[] args) throws IOException {
//...
return requestBuilder.get()
.tag(Invocation.class, new Invocation(method, argumentList))
.build();
}
RequestFactory.create#L92 他將接口調(diào)用的方法和參數(shù)列表包裹到Invocation
對(duì)象中好港,放在了okhttp3.Request.Builder
的tag里了愉镰。簽名的首選也是攔截器,限于2.4.0前無(wú)法獲得方法和參數(shù)列表钧汹。而使用Retrofit 2.5.0只需要在攔截器中獲取到Invocation
對(duì)象丈探,后續(xù)的簽名如上文所述。然而這個(gè)功能似乎沒(méi)有現(xiàn)在其change log 中寫出來(lái)拔莱。
小結(jié)
Retrofit是一個(gè)非常好的開(kāi)源網(wǎng)絡(luò)請(qǐng)求框架碗降,非常值得研究。筆者為求實(shí)現(xiàn)而修改的拙劣代碼在優(yōu)雅的Request.Builder.tag
面前不值一提塘秦。謹(jǐn)以此短文來(lái)記錄與大神的差距讼渊。