添加一个自定义拦截器很简单,只需要实现 Okhttp 的 Interceptor 接口,重写其中的intercept 方法,最后在 OkHttpClient.Builder 链式代码中注册和添加这个拦截器,为什么要称作“自定义“拦截器呢?因为 Okhttp 核心实现就是基于其内部五大拦截器,我们下文再说。
需要注意的是,Retrofit 是基于 Okhttp 的封装,所以当我们谈论“Retrofit的拦截器”,其实就是OKhttp的拦截器。
先看一下上一节,我们用 Okhttp 实现网络请求的 demo
对照着看一下 Okhttp 发送请求的执行流程
我们分析大红框里的内容,这张图有些繁琐,也可以转化成下图。
还是直接上图,就像流水线工厂一样,一个一无所有的 Request,依照顺序依次经过五大拦截器的前置拦截,最后拿到response后,再倒序经过五大拦截器的后置拦截。
我们引用@yuashuai 所举的例子,他表述的既生动又贴切。
老张有很多干面条,但是他想吃汤面,可是自己又不会做,但是碰巧村里大郎会做,于是老张拿一包干面条让大郎做成了汤面。但是老张发现他做面不好吃,盐都没放,连个青菜叶子都没有。
这时候老张正好碰到隔壁老王,老王说了这东西我也会做,比他做的好吃多了。于是老张又拿着一包干面条给了老王,老王说老张你等着,我马上回家给你做,做好了就给你送过去。但是老王回家并没有做,而是去家里拿了一包盐,然后去找隔壁老李了,原来老王并不会做面,但是他知道隔壁老李会做,而且做得比较好吃。于是他把干面条和盐都交给了老李。老李对老王说你回去等着吧,做好了马上给你送过去。可是老李同样不会做,但是他知道村里的大郎会做,这时老李首先回厨房拿了两个生菜叶子,然后带着老王给的干面条和盐去找大郎了,对大郎说,生菜叶子,盐,面条都给你了,你快给我做一碗面。大郎对老李说好嘞,3分钟就好了,3分钟后,老李拿着做好了的放了盐和生菜叶子的一碗面回去了。本来打算直接给老王,但是一想,自己放了两个生菜叶子,不吃点这个面吃不是有点亏,于是老李偷偷了吃了几根面。然后老李去找老王说你的面做好了并把面交给了老王。老王一看这面只有两个青菜叶子,营养是不是不够呀!于是老王又买了半斤熟牛肉,切切放了进去。然后老王去找老张说你的面做好了,还说道这么大一碗你也吃不完吧,让小张也吃点。最后老张吃着老王送来的红烧牛肉面感动的肉牛满面。
这里的干面条就可以看做一个最原始的 request,到老王哪里被加了点盐,到老李哪里被加了生菜叶子,于是大郎才能把这个request做成放了盐和生菜叶子的response,这个response回到老李哪里又被啃了几口,到老王哪里又被放了点牛肉。于是最后回到老张那里收到的response就是被扣了几口并且加了牛肉的response。这样整个链条是不是就清楚了!
@Wanghao 画了一张非常全面的图。
在第三讲的末尾我们讨论过 BridgeInterceptor 拦截器,它会对我们我们配置的 request请求补充头信息,比如请求类型、UA,如果你自己不加ua,Bridge 拦截器就会添加“okhttp/版本号”这样的ua。
这是 BridgeIntercetpor 的源码
我们也可以自定义拦截器,分为应用级别拦截器和网络级别拦截器,在 @Wanghao 的图上我们可以看到它们分别作用的时机。
1.应用级别拦截器:只会调用一次,获取到最终的 response 结果
2.网络级别拦截器:可以感知到网络的请求的重定向,以及重试会被执行多次
这两种拦截器在注册方式各不同,分别调用 addInterceptor()以及 addNetworkInterceptor 方法进行注册,但实现拦截器的步骤没有不同,所以我们不做过多笔墨。
我们随便找一个拦截器 demo 分析一下,我们需要熟悉拦截器的具体操作,找到它代码流程上的一些特征,然后根据这些特征在Jadx中搜索反编译后的 Java 代码,去寻找某书实现添加sign等等11个参数的拦截器。
除此之外还有个思路,上文说到,自定义的拦截器分为 Application 拦截器和 Network 拦截器,但不管哪一种,都需要在 OkHttpClient.Builder 链式代码中通过 addInterceptor/addNetworkInterceptor 方法添加和注册后才能生效,那我们可以全局搜索 addInterceptor 以及 addNetworkInterceptor,然后得到拦截器的线索,这样应该也是可以的。我们先看看当前的这个思路。
我们可以看到三个明显的特征:
一: 是实现Interceptor接口 “public class xxx(自定义的拦截器名) implements Interceptor {”
二: 是重写intercept方法“public (可能有修饰符) Response intercept(Chain Chain)……”
三: 是添加字段时调用的 “.addQueryParameter” 方法(这儿我们加了个“.”,可以缩小检索范围),如果是添加Header 头信息,则会调用 ”.header“ 这个方法
如果 App 没有对 Okhttp 类进行混淆,那我们就可以根据这三个特征找到 Retrofit 的拦截器实现。
我们这儿再多说两句,看图中第九行,Request originalRequest = chain.request(); 这个demo作者为什么要将这个Request命名为 originalRequest(original:原先的,原始的)呢?
再回想一下拦截器的工作原理。想象一下一个流水线,request 是材料,response 是产品,从头到尾经历了许多次加工(拦截器)。每个拦截器先通过 chain.request() 得到这个 reqeust,经过一系列操作后的 request,再放回 chain.proceed 方法,最后return 回去,request 经过了一次洗礼,等待它的是不断的新的洗礼。
chain.proceed 这个方法名非常精准和有神,Okhttp采用了设计模式中的责任链模式,感兴趣的可以看一下这篇关于责任链的文章https://www.cnblogs.com/aeolian/p/8888958.html。
因此,对于每一个拦截器来说,取到的request就是原始和过去的,命名就可能会用”old“、”original“,返回到reqeust就可能用”final“、”new“修饰,解释到这儿,我想大家对拦截器应该有了一个具象的了解了。
上面我们找到了三个特征,那么接下来我们通过 Jadx 的全局搜索功能,开始寻找某书 Retrofit的拦截器,进而找到 sign 等参数的实现。如果你的电脑内存只有 8G,那么接下来的这一步操作对你十分有用,除此之外,它对我们搜索关键代码点也有奇效。
反编译一个App,你会看到成千上万的类和方法,我们可以简单将这些类和方法分成“具体业务逻辑的代码”和“App架构和工具的代码”,前者的类名一般是包名.xxx,比如某书App包名为 com.xingin.xhs,它的业务代码就是 com.xingin.xxxx;而第二类往往五花八门,比如 Android本身的一些方法类 “android.xx”,腾讯的sdk“com.tencent.xxx”,微博的登录接入sdk“com.weibo.xxx”。
我们想要定位到的内容/加解密逻辑等等,基本都在前者的类里,而后者既缭乱人眼,烦人心神,又占用内存,Jadx给我们提供了屏蔽这些类的方法,屏蔽后Jadx将不再反编译这些类,你也无法再跳转到该方法里,或者在全局检索时看到这些类中扰人的代码。
方法很简单,只需要在类列表中选中某个类,右键 exclude 即可屏蔽这个类。
但我们总不可能点几百几千下,这样实在太麻烦了。Jadx 的设计者给我们提供了一种类似正则表达式的匹配方法,可以屏蔽这些讨人厌的第三方类,操作也很简单。打开文件——首选项,如下图,(建议)不要勾选“自动后台反编译”,因为它可能占用你电脑更多的内存。
点击编辑,像我这样输入,类名以 baidu 或 android 开头的类就会被 Jadx 屏蔽掉,点击确定后保存设置,Jadx 会重新加载这个 Apk,你会发现已经生效,这些类变成了灰色。
我们浏览一下列表,将比较明显的一些刺头并闭掉。
android
com.alipay
com.baidu
com.facebook
com.google
com.huawei
com.meizu
com.networkbench
com.qiniu
com.tencent
com.vivo
kotlin
你可能会难以把控,怕误伤,比如 com.networkbench.xxx,它似乎很像 App 中用于处理网络请求的模块,怎么避免误伤呢?百度一下就行,如果消息模棱两可,就搁在那儿不排除即可。
android com.alipay com.baidu com.facebook com.google com.huawei com.meizu com.networkbench com.qiniu com.tencent com.vivo kotlin
一路保存设置,最好重启一下JADX,这样可以释放掉由于反复操作Jadx的设置而导致的内存占用。
Jadx默认在使用搜索功能时,才开始反编译,点击Jadx左上角魔法棒,然后稍等片刻。
原先反编译某书时占用6G左右内存,现在少了几乎一半。
经过这一顿调教
我们正式开始搜索拦截器相关的特征。
Jadx 并不支持正则表达式的方式进行检索,所以我们这儿搜索 “implements Interceptor {”
再试试第二个关键词
再试试第三个关键词
真是让人惊喜,只有三处适宜的代码,而且其实就在两个类里,先看一下第一个类
传入了一个 map 集合,不停 for 循环,按照键和值的方式将内容传进 addQueryParameter 方法中,妙啊,比如map为 {“sign:12345678”},这不就传进去了吗,赶紧右键查找一下用例,只有一处,点进去。
代码越来越乱了,这个时候冷静一下,要避免一叶障目,不要先钻进去研究逻辑,保持怀疑,然后直接看下一个符合条件的类,要知道这儿可是出现多处 .addQueryParameter 方法,也很像添加多个参数的操作。
点进去看一下,有些代码会让你觉得很 demo 很像,比如什么request.url().newBuilder(),除此之外,我们看到了大量类似 oldRequest 的提示,这也只能说明这个地方很可疑,就像第一个检索内容一样,但不足以让我们认定它就是我们要找到拦截器。
从上往下看一下这个类
这儿似乎就是我们需要的那个拦截器类,我们看一下sign的用例,j上右键——查找用例
两处方法点开后,仔细瞅瞅,不管是map的put方法,还是newBuilder的add方法,其实都是往集合里塞东西,里面都是两个参数,迷糊的同学可以看一下前面的demo,参数一是键,也就是参数名,在这儿也就是this.j,也就是”sign“,参数二就是具体的sign值,也就是我们要破解的sign。我们可以发现,两处的sign值都是由a方法生成,a方法的传入是一个map集合(linkedhashmap是map的一种),返回一个String。
多半这个a方法就是sign的生成处了,在下一讲中,我们会验证一下是否属实,如果没找错的话,就可以分析这个 sign,进行 sign 的破解。这篇文章就到此为止了。
看我教程的人不多,但读者一直支撑着我继续写这些拙劣的文字和教程,非常非常感谢大家。
在这儿做一下更新的说明,我每一节的内容其实量挺大的,再加上我需要反复琢磨表达和思路,所以更新比较慢,以后会减少单篇的内容,在保证质量的前提下尝试一天一更,谢谢三五读者的支持和厚爱
利用 Flask 动态展示 Pyecharts 图表数据几种方法
Flask 蓝图机制及应用
哪吒到底有多火?Python数据分析告诉你!