本文内容其实是不适合发出来的,希望大家切勿用作商业用途,也切勿将功能发布到线上环境。技术一定是为生活服务的,是为了大家共同的美好生活。判断一件事是否值得做,一定是利人利己,错误的事可能舒服自己而痛苦了别人,损人利己的事千万不可为。
在测试功能时,我们可能会遇到一些偶现崩溃的情况,往往难以复现。在修改代码时,有时往往改了这个 Bug ,在某个其他地方又引发了另一个 Bug,或者又是不经意间修改了 UI 样式界面。当然以上问题,我们可以看日志跟踪,也可以多写单元测试。基于等等场景,我们部门共建了一个 sdk ,我也是其中的一员。
其中重要的一个环节,复现 bug 时需要还原当时现场。也就是说要还原当时测试同学的操作步骤,需要还原当时请求的网络数据,需要还原数据库,需要还原 SharedPreferences ,如果这些现场都能还原,就很有可能复现并解决这个奔溃。而这里还涉及到账号还原,也就是说要还原当时测试同学登录的账号,我这里说的是在另一台手机上自动切换登录。
同时基于以上功能,我们就可以事先录制很多正常操作路径,也可以把当时的每个界面布局录制下来。这样每次发布之前,都在云平台上跑一遍,就能测试发现很多问题。如何还原网络数据、还原数据库等现场本文暂时不讲,本文主要来分析如何还原账号信息。温馨提示,只要对源码足够熟悉,这些都不是事。
PCG 部门的所有产品都是比较成熟的产品,任何一个产品都有亿量级的用户,我们写的 sdk 是无法侵入业务代码的。也就是说业务开发的同学在 Application 中配置一个入口,以上这些功能就都要能实现。现在回到账号还原上来,如果要还原账号现场,那么必定会有账号拦截与自动切换登录,而这整个过程,业务上层是不会给我们适配代码的。因为腾讯视频、应用宝与腾讯新闻等等,整个 PCG 的应用都需要集成我们的 sdk。我们在写代码的时候一定要考虑通用性、适配性与集成成本等等。只是该功能只在测试环境集成。
PCG 应用业务侧登录都是用的 QQ 与微信第三方登录,我们规定测试同学只能用 QQ 授权登录,方便自动拦截切换登录实现。那么接下来第一步就是如何拦截登录的账号信息,我之前考虑过只拦截 QQ 授权时的信息,但后面发现授权信息的 token 会有过期时间,后面就果断放弃了。所以要实现该功能且要做到所有应用都通用,就只能想办法拦截到 QQ 授权时的用户名和密码。
怎么在不侵入业务逻辑代码情况下,拦截到 QQ 授权时的用户名和密码呢?估计大部分人都会认为无法实现,但其实只要对源码够熟悉,分析实现起来还是挺简单的。业务侧普通的授权方式是没有输入用户名和密码的过程,我们想要拦截这些信息,势必需要用户有这个主动的操作过程,基于这点我们就需要引导用户跳转到 H5 的授权界面。那这还不简单,我们在业务逻辑中直接打开 QQ 的 H5 授权不就可以了?但问题是我们不能改业务逻辑代码,而且该 sdk 也不会上线,因此我们只能在 Application 初始化 sdk 里面中去做处理。
那么怎么才能在不该动原有业务逻辑情况下,点击 QQ 授权是自动跳转到 H5 授权界面呢?我们势必要去翻一下 QQ 登录提供的 sdk 源码,发现其中会判断有没有安装 QQ 应用,如果没有则是提示下载 QQ ,如果有安装 QQ 则会跳转到 AgentActivity 进行授权。因此我们只要想办法欺骗 QQ 的授权 sdk 就行,当调方法问有没有安装 QQ 时,我们返回安装了;当启动 AgentActivity 授权时,我们偷偷的将其引导到 H5 授权界面去输入用户名和密码,只有这样我们才有机会拦截到用户名和密码。那么如何才能欺骗呢?这就取决于我们对源码的熟悉程度了。
// PMS
private HandlerInvokeCallback mPMSCallback = new HandlerInvokeCallback() {
@Override
public void beforeInvoke(Method method, Object[] args) {
try {
// 一般上层业务逻辑中会有判断 QQ 有没有安装
if (CommonUtils.equals("getPackageInfo", method.getName())) {
if (CommonUtils.equals(args[0], QQ_PACKAGE_NAME)) {
// 替换成当前应用的包名,无论是否安装 QQ 返回都是安装
args[0] = mApplication.getPackageName();
}
}
// SDK 中会查询 QQ_OAUTH_ACTIVITY ,默认返回 QQLoginH5AuthorizeActivity
if (CommonUtils.equals("queryIntentActivities", method.getName())) {
Intent queryIntent = (Intent) args[0];
ComponentName componentName = queryIntent.getComponent();
if (componentName == null) {
return;
}
String queryClassName = componentName.getClassName();
if (CommonUtils.equals(queryClassName, QQ_OAUTH_ACTIVITY)) {
args[0] = new Intent(mApplication, QQLoginH5AuthorizeActivity.class);
}
}
} catch (Exception e) {
e.printStackTrace();
LogUtil.w(TAG, "pms beforeInvoke exception: " + e.getMessage());
}
}
@Override
public Object afterInvoke(Method method, Object[] args, Object returnObj) {
return returnObj;
}
};
// AMS
private HandlerInvokeCallback mAMSCallback = new HandlerInvokeCallback() {
@Override
public void beforeInvoke(Method method, Object[] args) {
try {
// 把跳转到 QQ 原生的页面,都替换跳转到 QQLoginH5AuthorizeActivity
if (CommonUtils.equals("startActivity", method.getName())) {
Intent intent = (Intent) args[2];
ComponentName componentName = intent.getComponent();
if (componentName == null) {
return;
}
if (CommonUtils.equals(componentName.getClassName(), QQ_OAUTH_ACTIVITY)) {
intent.setComponent(new ComponentName(mApplication, QQLoginH5AuthorizeActivity.class));
}
}
} catch (Exception e) {
e.printStackTrace();
LogUtil.w(TAG, "ams beforeInvoke exception: " + e.getMessage());
}
}
@Override
public Object afterInvoke(Method method, Object[] args, Object returnObj) {
return returnObj;
}
};
// Application 中初始化入口
@Override
public void init(Application application) {
this.mApplication = application;
SharedPreferences accountSp = mApplication.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
mAccountSpEditor = accountSp.edit();
// 读取之前存的 QQ 用户名和密码
mQQAccount = accountSp.getString(SP_QQ_ACCOUNT_KEY, "");
mQQPwd = accountSp.getString(SP_QQ_PWD_KEY, "");
mAppId = accountSp.getString(SP_QQ_APP_ID_KEY, "");
mAuthLoginType = accountSp.getInt(SP_QQ_AUTH_TYPE_KEY, 0);
// 手Q应用适配另一套
if (CommonUtils.equals(application.getPackageName(), QQ_PACKAGE_NAME)) {
adapterInterceptQQAccount();
} else {
hookPMSAndAMS();
}
LogUtil.i(TAG, "init read account info: " + mQQAccount + ", " + mQQPwd + ", " + mAppId + ", " + mAuthLoginType);
}
只要跳转到了 H5 的授权页面,拦截用户名和密码就很容易了。其实开发中我们看似很多实现不了的功能,只要我们静下心来去分析,还是能够实现的。这其实还是一个比较简单的功能,再扩展一些像还原网络现场,势必需要拦截监控用户的网络,而微视用的 wns 、手Q用的是 msf、应用宝用的是自己修改的 OkHttp 千奇百怪。
其实生活中总会有些事,不管我们有多少困难,有多少委屈,有多少艰苦,我们做下去,我们最终必然会感觉到骄傲。
视频地址:https://pan.baidu.com/s/1COarOAlmOPCUlKsazho0Cw
视频密码:8lr0
作者:红橙Darren
链接:https://www.jianshu.com/p/fd631b360c5b
关注我获取更多知识或者投稿