Travis.Wang

黑盒分析iOS版QQ-SSO登录过程, 帮你的App减肥!

最近一直在忙FIR.im 今晚找点时间回想一下以前的积累(最近看iOS开发的比较少了), 可能现在已经过时了. 不过应该还具有一些参考意义.

注: 大部分内容的只写了思路, 不保证代码能让复制粘贴党跑通.

为什么

  1. QQ实现SSO根据官方文档, 从来没有成功过, 除了QQ自己的一些游戏外, 也基本没见过有第三方的app可以直接用QQ登录.
  2. 官方的SDK极其硕大(官方文档:9.1M (编译后的应用安装包增加1.3M)), 对于我等有些项目洁癖的人来说, 为了实现用QQ登录这个简单需求, 有些抓狂!

分析过程

微博的SSO过程比较透明, 是开源的, 而且数据传递过程比较简单, 就是通过Query参数来回传递数据.

不幸的是QQ不是这么做的, 否则我也不用写这篇博客了.

开始截获回调回来的url参数, 发现里面就只有一个参数generalpastboard=1. 很明显的提示就是: 我们是用系统粘贴板了! 而

其实分析SDK要比反编译app容易的多, 比较SDK我们可以调试, 很容易进入调用栈进行分析.

用nm获取静态lib里包含的方法名, 然后grep一下Pasteboard:

$ nm TencentOpenAPI | grep board

00000428 t -[QQObjectPasteboard objectFromGeneralPastboard]  
000002e7 t -[QQObjectPasteboard setObjectByGeneralPastboard:]  
00000000 t -[QQPasteboard initWithName:]

随便一看就找到几条可以的方法, 大概可以猜到QQObjectPasteboard这个类是负责读写系统粘贴板数据的.

中间饶了些弯路:

从分析QQ官方SDK开始, 下载下了的SDK包括了几个文件, 其中TencentOpenApi_IOS_Bundle.bundle是资源文件, 里面有个js.zip很扎眼, 因为js==开源 !

而且打开看了一下: 不仅开源, 还注释的非常清楚, 大公司, 要求很规范嘛!

/**
 * 接口命名空间
 */
window.tencent = tencent = {};  
/**
 * 本地接口命名空间
 */
window.local =local = {};  
/**
 * 常量命名空间
 */
window.constant = {};  
/**
 * 工具类命名空间
 */
window.utils = {};

通过搜索SSO 找到了一个constant值 kOPENQQAPI_SCHEMA: "mqqOpensdkSSoLogin", 确定了这个就是打开QQ的schema, 然后进行了一通搜找到了启动QQ的URL拼装方法:

startQQ:function(dic){
    var QQURl="mqqopensdkapiV2://";
    var url=ios_constants.kOPEN_QQ_API_SCHEMA+"://"+ios_constants.kOPEN_QQ_HOST+"/"+loacl_appSchema+"/";
    return jump.startApp(QQURl,dic,url,"QQ");
}

后来一想, 用不着这样啊, 于是回到正路.

回到正路

忽然想到, 我可以冒充QQ! 这样就可以知道谁调用了我, 传给我什么参数.

后来发现QQ在读完了粘贴板上的数据后就立即删除了, 幸亏这么做, 要不然可能要浪费好多时间去找粘贴板上的数据.

先把QQ删除, 写一个小demo, 给它设置url scheme: mqqOpensdkSSoLogin 这样第三方的App就可以把你的demo当作是QQ了, 在App Delegate里的handleOpenURL的方法里打开断点, 然后打开一个第三方app, 比如天天**之类的 选择用QQ登录, 这个时候demo在断点停下了, 这个时候 我们就可以知道我们需要给QQ传递哪些url参数了,大概是这样:

mqqOpensdkSSoLogin://SSoLogin/<callback_uri>/<com.tencent.你的应用id>?generalpastboard=1",

然后, 再加一个遍历系统粘贴板的循环, 可以轻松的找到 com.tencent.你的应用id 这个粘贴板类型的数据.

然后我们就可以进行下一步了.

读数据

找到了数据, 首先想到的是解密. 我们都知道QQ的加密还是做的很不错的, 直接http传输加密过的数据, 反而比完全信赖https要好很多, 因为https很容易用自制根证书中间人抓包和篡改数据.

其实, 要是在SDK里解密, 就要把密钥写进SDK, 而且随着以后版本的更新等各种不稳定因素, 数据被解密事小, 要是把密钥泄漏了可能就要出问题了.

先简单试一下, 直接读, log出来是bplist, 了解了, 这是把字典或者数组archive后直接赋值给粘贴板了, 10环!

回调数据也是一样的原理, 就不再啰嗦了.

这样, 发出去和收回来的数据都齐了, 我们就可以直接在我们的app里调用QQ SSO了!

登录实现

1. 打开手机QQ:

//安装官方说明, 填写需要获取用户数据的权限范围.
NSString *scope=@"get_user_info,add_share";

NSString *appid=@"你的QQ应用ID";

NSDictionary *bld=[[NSBundle mainBundle] infoDictionary];
NSString *bundleId= [bld objectForKey:(NSString*)kCFBundleIdentifierKey];
NSString *appName= [bld objectForKey:(NSString*)kCFBundleNameKey];

UIDevice *device=[UIDevice currentDevice];

NSDictionary *dict=@{
                     @"app_id" : appid,
                     @"client_id": appid,
                     @"app_name" : appName,
                     @"bundleId" : bundleId,
                     @"status_machine" : [device model],
                     @"status_os" : [device systemVersion],
                     @"status_version" : [[device systemVersion] substringToIndex:1],
                     @"scope":scope,

                     //目前这些不需要变
                     @"response_type":@"token",
                     @"sdkp" : @"i",
                     @"sdkv" : @"2.0",
                     @"jsVersion":@"20080",
                     };

//关键点, 把相关数据添加到系统粘贴板上
NSString *pbType=[@"com.tencent." stringByAppendingString:appid];

[[UIPasteboard generalPasteboard] 
     setValue:[NSKeyedArchiver archivedDataWithRootObject:dict]
     forPasteboardType:pbType];

//拼装调用QQ的URL
NSString *appAuthURL =[NSString 
        stringWithFormat:@"mqqOpensdkSSoLogin://SSoLogin/%@/%@?generalpastboard=1",
        callback_uri,
        pbType];

//打开QQ
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:appAuthURL]];

2. 手机QQ回调

NSString *scheme=url.scheme;
NSString *pbname=[NSString stringWithFormat:@"com.tencent.%@",scheme];

//从粘贴板读出数据
UIPasteboard *pb=[UIPasteboard generalPasteboard];
id value=[pb valueForPasteboardType:pbname];

//读出archive的数据
params= [NSKeyedUnarchiver unarchiveObjectWithData:value];

//我们也会把数据删掉, 保持跟SDK一样的行为
[UIPasteboard removePasteboardWithName:pbname];

if (params && [params isKindOfClass:[NSDictionary class]]) {
    NSString *token=params[@"access_token"];
    NSString *expires=params[@"expires_in"];
    NSString *uid=params[@"openid"];

    //到此为止 你已经得到了token,超时时间,用户id
    // and have fun!

}

最后

鉴于QQ需要做向前的兼容, 即便是新的SDK更改了认证方式, 我相信, 这里提到的这些方法应该还是可以继续使用的.


不知不觉写了这么长一篇, 其实只是希望QQ的SDK或者官方文档能更好用一些, 或者开源一下认证方法, 就为了一个SSO, 给项目塞进去这么打个SDK太不爽了.

转载请注名出处 http://imi.im
欢迎大家微博交流 @trawor 或者 去 http://fir.im 玩耍