百度贴吧签到请求分析

有段时间对贴吧比较着迷,比如:搞笑吧,帝吧,s_cu*e官网吧(你懂的),各种涨知识啊!
于是每天必然签到,然后发现了手机的贴吧客户端有“一键签到”功能,顿时感觉这是业界良心啊!
但是一用之下却发现了各种坑爹,比如抢签的时段(凌晨0点-1点)不能用,7级以下的吧不能用……
实在让人痛心疾首,捶胸顿足,大呼苍天不公!

当时怀着满眶热泪,在i9305下载了Android版的贴吧简版(这个2M,正式版21M),配合Fiddler的HTTP代理抓包,分析它的登陆和签到机制,然后再写个小工具模拟签到行为……妈妈再也不用担心我的签到了。

废话就是多,下面入正题。

一、输入密码点击“登录”

拦截到的请求如下:

POST http://c.tieba.baidu.com/c/s/login HTTP/1.1
Charset: UTF-8
Content-Type: application/x-www-form-urlencoded
Accept-Encoding: gzip
User-Agent: Dalvik/1.6.0 (Linux; U; Android 4.1.2; GT-I9305 Build/JZO54K)
Host: c.tieba.baidu.com
Connection: Keep-Alive
Content-Length: 376

_client_id=wappc_1407061932425_634&_client_type=2&_client_version=4.2.7&_is_mini=1&_phone_imei=354720056708957&cuid=89A7270749464157876426AB985C394E%7C759807650027453&from=xy0038&isphone=0&model=GT-I9305&net_type=3&passwd=MTIzNDU2Nzg5&stErrorNums=0&stMethod=1&stMode=1&stSize=92&stTime=190&stTimesNum=1&timestamp=1407078191454&un=codemoon&sign=55FF3AAEA6CCA5C35B8B234272ADD58B

把Post的数据整理好看一点:

_client_id wappc_1407061932425_634  //由另一个页面的JSON返回提取出来的,后面说
_client_type 2 //这个是在客户端里定死了的
_client_version 4.2.7 //这个是看客户端的版本啦
_is_mini 1 //这个是在客户端里定死了的
_phone_imei 354720056708957 //典型的手机IMEI号啦
cuid 89A7270749464157876426AB985C394E|759807650027453 //后面说
from xy0038 //估计可以乱写,这个是在手机储存目录下的tieba文件夹里的from.dat文件里面的字符,具体来源要追寻到apk文件里面的assets目录下的channel文件。
isphone 0 //这个是在客户端里定死了的
model GT-I9305 //这个很明显可以乱来,设备型号, Android设备 Build.MODEL
net_type 3 //网络类型,WIFI为3,CMWAP为2,CMNET为1
passwd MTIzNDU2Nzg5 //密码的Base64编码
stErrorNums 0 // 客户端运行期间出现的SocketException,SocketTimeoutException异常总数。弄成0比较好。
stMethod 1 //HTTP请求类型,1为POST,2为GET
stMode 1 //0=UNAVAIL,1=WIFI,2=TwoG,3=ThreeG,可以看出是网络类型
stSize 92 //读取的字节数??
stTime 190 //这个是一个时间差,毫秒级,具体没有仔细分析
stTimesNum 1 //??,可能的值似乎就1和2
timestamp 1407078191454 //这个你懂的,当前时间,Java里是Long.toString(System.currentTimeMillis())
un codemoon //用户名
sign 55FF3AAEA6CCA5C35B8B234272ADD58B //最重要的一个参数,后面说

这些请求的参数呀,我花了两个晚上把APK反编译跟踪分析,结果发现大部分参数竟然是没用的!我简直是用生命在作死……不过也好,我作死了大家就知道别作死了。

注意,如果上面的请求成功的话,就会返回JSON,注意BDUSS,tbs,id这三个值:

{
"user": {
"id": "19831869",
"name": "codemoon",
"BDUSS": "TRnVVJ4akx5YTE0QVdJMzVkUHFhb09XQ0ZRcWFWaEVXSHJnNVVOaHJVWkQzQVZVQVFBQUFBJCQAAAAAAAAAAAEAAAA9nC4BY29kZW1vb24AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAENP3lNDT96TH|73327be05e85c1dd70c22692aa054516",
"passwd": "qw_xwdQ5fhAF5272+9Em\/TboIM15fOlXyIQpgPNveyZzXBZ\/myWkJmMI0nPURrqTu4q5KRC9mwGs4WbZxDpF0hqJ3dlXqmgE5tFEz0NbRwHrzMF649jgv9WyzizZ2BqgUad0ry5ijcFKFLAE+7gTzuERddbrpXXT7H075WX2rT35dbWbsviv1F7E04G3383GYl4VmAKITWPg3PRzvs4RuOXe2TuyADiuS5ilQleCUgssOGYs2lgLhKrRSapiR44d7CCDqDWS7BY4uk=",
"portrait": "3d9c636f64656d6f6f6e2e01"
},
"anti": {
"tbs": "2cb25d0a57a7e9f51407078211"
},
"server_time": "356847",
"time": 1407078211,
"ctime": 0,
"logid": 211800684,
"error_code": "0"
}

二、部分参数详解

cuid参数是什么? ——我作死之后发现的非必须参数,不感兴趣的请跳过。

看Java代码描述如下:

public static String getCUID(Context paramContext){
String str1 = getDeviceId(paramContext); //取设备的ID号
String str2 = DeviceId.getIMEI(paramContext);
if (TextUtils.isEmpty(str2)) //获取设备IMEI,取不到就为0(IMEI就是15位串号,大家请跟我一起按*#06#)
str2 = "0";
//把取到的IMEI反转排列
String str3 = new StringBuffer(str2).reverse().toString();
return str1 + "|" + str3; //ID号 加 竖线 加 IMEI反转
}

cuid值分两部分,竖线后面部分可以是0;竖线前面的话,估计随便弄一堆[0-9A-Z]的垃圾应该也成,因为不是每台使用贴吧的设备都有DeviceID的。

sign参数是什么? ——作死之后发现的必须参数!

它是将POST请求的参数全部用param=value的形式连起来,最后加上 tiebaclient!!!,再转成UTF-8字节流,然后用MD5算法,转成大写的一串MD5字符,Java代码大致如下:

String paramString = "_client_id=wappc_1407061932425_634_client_type=2_client_version=4.2.7_is_mini=1_phone_imei=354720056708957cuid=89A7270749464157876426AB985C394E%7C759807650027453from=xy0038isphone=0model=GT-I9305net_type=3passwd=MTIzNDU2Nzg5stErrorNums=0stMethod=1stMode=1stSize=92stTime=190stTimesNum=1timestamp=1407078191454un=codemoon"; 

paramString.append("tiebaclient!!!"); //POST请求的参数串末尾加上"tiebaclient!!!"
String sign = a1(paramString); //计算paramString的MD5值,并把MD5转成字符串,就是sign的值了

具体的计算过程代码如下所示,没什么兴趣的赶紧跳过。

public static String a1(String paramString){
try{
String str = a2(new ByteArrayInputStream(paramString.getBytes("UTF-8")));
return str;
} catch (Exception localException) {

}
return null;
}

//MD5计算
public static String a2(InputStream paramInputStream) {
if (paramInputStream == null) return null;
try{
byte[] arrayOfByte = new byte[1024];
MessageDigest localMessageDigest = MessageDigest.getInstance("MD5");
while (true) {
int j = paramInputStream.read(arrayOfByte);
if (j <= 0){
String str = a3(localMessageDigest.digest());
return str;
}
localMessageDigest.update(arrayOfByte, 0, j);
}
}catch (Exception localException){
ac.a("StringHelper", "ToMd5", localException.toString());
return null;
}finally{
f.a(paramInputStream);
}
}

//字节数组转换成字符串
public static String a3(byte[] paramArrayOfByte) {
static final char[] g = { 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 65, 66, 67, 68, 69, 70 };
if (paramArrayOfByte == null) return null;
StringBuilder localStringBuilder = new StringBuilder(2 * paramArrayOfByte.length);
for (int j = 0; ; j++){
if (j >= paramArrayOfByte.length)
return localStringBuilder.toString();
localStringBuilder.append(g[((0xF0 & paramArrayOfByte[j]) >>> 4)]);
localStringBuilder.append(g[(0xF & paramArrayOfByte[j])]);
}
}

_client_id是什么? ——作死后发现的非必须参数,不知道啥时候会用到,还是介绍一下。

按下面的地址请求一次,返回的JSON里面含有_client_id值:

POST http://c.tieba.baidu.com/c/s/minisync HTTP/1.1
Charset: UTF-8
Content-Type: application/x-www-form-urlencoded
Accept-Encoding: gzip
User-Agent: Dalvik/1.6.0 (Linux; U; Android 4.1.2; GT-I9305 Build/JZO54K)
Host: c.tieba.baidu.com
Connection: Keep-Alive
Content-Length: 327

_active=0&_client_type=2&_client_version=4.2.7&_is_mini=1&_msg_status=1&_os_version=4.1.2&_phone_imei=354720056708957&_phone_screen=720%2C1280&_pic_quality=2&cuid=89A7270749464157876426AB985C394E%7C759807650027453&from=xy0038&model=GT-I9305&net_type=3&stErrorNums=0&timestamp=1407320443010&sign=79378EC4475436C7A6F48512AAA3B57F

整理好看一点:

_active 0 //不清楚
_client_type 2 //不再说了,上面有
_client_version 4.2.7 //客户端APK版本,实测可以作死乱写,服务器用来提示新版
_is_mini 1 //是否迷你版的贴吧
_msg_status 1 //可能的值我见过1和0
_os_version 4.1.2 //Android系统版本,实测可以作死乱写
_phone_imei 354720056708957 //设备IMEI
_phone_screen 720,1280 //屏幕宽度,屏幕高度,单位为px
_pic_quality 2 //看名字是图像传输品质
cuid 89A7270749464157876426AB985C394E|759807650027453 //上面说过
from xy0038 //上面说过
model GT-I9305 //设备型号
net_type 3 //网络类型,WIFI为3,CMWAP为2,CMNET为1
stErrorNums 0 //上面说过
timestamp 1407320443010 //时间戳,上面说过
sign 79378EC4475436C7A6F48512AAA3B57F //上面说过

返回的JSON比较长,关键部分如下:

"error_code": "0",
"client": {
    "client_id": "wappc_1407320464879_997"
},
"version": {...... //省略

JSON读取、正则匹配或者indexOf substr什么的,各显神通把client_id弄出来就可以了。

好了,说了一大堆,总结一下有用的东西:client_id 和 timestamp 还有最重要的 sign值计算方法


三、点击“我喜欢的吧”拦截到的请求如下

(如果你想做个有详细界面显示某吧的等级,经验值,头像什么的,就要仔细研究这一步的请求,如果只是想做个自动登陆签到的,看下一步会比较好。)

POST http://c.tieba.baidu.com/c/f/forum/like HTTP/1.1
Charset: UTF-8
Content-Type: application/x-www-form-urlencoded
Accept-Encoding: gzip
User-Agent: Dalvik/1.6.0 (Linux; U; Android 4.1.2; GT-I9305 Build/JZO54K)
Host: c.tieba.baidu.com
Connection: Keep-Alive
Content-Length: 572

BDUSS=VucDlIREJHWnN6dU5xcjloalJwUHhZRUZjRG0tRkVaUDRldkdUYzM0bUtOQXhVQVFBQUFBJCQAAAAAAAAAAAEAAAA9nC4BY29kZW1vb24AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIqn5FOEp-RTSl%7C62606e0bb98f50caa59619508d4bdac3&_client_id=wappc_1407061932425_634&_client_type=2&_client_version=4.2.11&_is_mini=1&_phone_imei=354720056708957&cuid=89A7270749464157876426AB985C394E%7C759807650027453&from=xy0038&model=GT-I9305&net_type=3&stErrorNums=0&stMethod=1&stMode=1&stSize=29482&stTime=554&stTimesNum=1&timestamp=1407505646088&sign=10B76797085A4300A1AEFB241491835C

把Post的数据整理好看一点:

BDUSS VucDlIREJHWnN6dU5xcjloalJwUHhZRUZjRG0tRkVaUDRldkdUYzM0bUtOQXhVQVFBQUFBJCQAAAAAAAAAAAEAAAA9nC4BY29kZW1vb24AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIqn5FOEp-RTSl%7C62606e0bb98f50caa59619508d4bdac3
_client_id wappc_1407061932425_634
_client_type 2
_client_version 4.2.11
_is_mini 1
_phone_imei 354720056708957
cuid 89A7270749464157876426AB985C394E|759807650027453
from xy0038
model GT-I9305
net_type 3
stErrorNums 0
stMethod 1
stMode 1
stSize 29482
stTime 554
stTimesNum 1
timestamp 1407505646088
sign 10B76797085A4300A1AEFB241491835C

返回的JSON有点长,自己分析一下哟,有吧名,经验值,等级名称什么的:

{
"forum_list": [{
"id": "20975",
"name": "c++",
"favo_type": "0",
"level_id": "10",
"level_name": "\u4eae\u51fa25CM",
"cur_score": "2175",
"levelup_score": "3000",
"avatar": "http:\/\/imgsrc.baidu.com\/forum\/pic\/item\/a6efce1b9d16fdfa4d885f0bb58f8c5495ee7b6b.jpg",
"slogan": "\u6709\u95ee\u9898\u4e3a\u4ec0\u4e48\u4e0d\u5148\u95ee\u95ee\u9694\u58c1C\u8bed\u8a00\u5427\u5462\uff1f"
},
{
"id": "59099",
"name": "\u674e\u6bc5",
"favo_type": "0",
"level_id": "9",
"level_name": "\u9ad8\u5927\u5bcc\u5e05",
"cur_score": "1722",
"levelup_score": "2000",
"avatar": "http:\/\/imgsrc.baidu.com\/forum\/pic\/item\/aec379310a55b319380291c141a98226cefc178e.jpg",
"slogan": "\u52aa\u529b\uff01\u4e3b\u5bb0\u81ea\u5df1\u547d\u8fd0\uff01\u4f17\u4eba\u7686\u5e1d\uff01"
},
{
"id": "14911",
"name": "\u641e\u7b11"
//此下省略一大段数据
//…………
//…………
}],
"server_time": "50585",
"time": 1407505668,
"ctime": 0,
"logid": 2868823358,
"error_code": "0"
}

四、可以从下面的页面获得喜欢的吧名和ID,更简短:

POST http://c.tieba.baidu.com/c/f/forum/favocommend HTTP/1.1
Charset: UTF-8
Content-Type: application/x-www-form-urlencoded
Accept-Encoding: gzip
User-Agent: Dalvik/1.6.0 (Linux; U; Android 4.1.2; GT-I9305 Build/JZO54K)
Host: c.tieba.baidu.com
Connection: Keep-Alive
Content-Length: 591

BDUSS=VucDlIREJHWnN6dU5xcjloalJwUHhZRUZjRG0tRkVaUDRldkdUYzM0bUtOQXhVQVFBQUFBJCQAAAAAAAAAAAEAAAA9nC4BY29kZW1vb24AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIqn5FOEp-RTSl%7C62606e0bb98f50caa59619508d4bdac3&_client_id=wappc_1407061932425_634&_client_type=2&_client_version=4.2.11&_is_mini=1&_phone_imei=354720056708957&ctime=1407506621182&cuid=89A7270749464157876426AB985C394E%7C759807650027453&from=xy0038&model=GT-I9305&net_type=3&stErrorNums=0&stMethod=1&stMode=1&stSize=1809&stTime=402&stTimesNum=1&timestamp=1407506621183&sign=70842D54C2815D2A6D09DEF811FA9572

返回JSON,注意id和name,签到的请求就会用到它们两个:

{
"forum_list": [{
"name": "\u6d41\u6d6a\u6cd5\u5e08",
"id": "2499734",
"is_like": "1",
"favo_type": "0",
"level_id": "9",
"member_count": "",
"avatar": "",
"slogan": ""
},
{
"name": "\u641e\u7b11",
"id": "14911",
"is_like": "1",
"favo_type": "0",
"level_id": "9",
"member_count": "",
"avatar": "",
"slogan": ""
},
{
"name": "\u7eaf\u97f3\u4e50",
"id": "130090",
//此下省略一大段数据
//…………
//…………

}],
"commend_forum_list": [],
"is_login": "1",
"page": {
"page_size": "20",
"offset": "0",
"current_page": "1",
"total_count": "0",
"total_page": "0",
"has_more": "0",
"has_prev": "0"
},
"anti": {
"tbs": "6777c0cc9d7103691407506646"
},
"server_time": "36958",
"time": 1407506646,
"ctime": 1407506621182,
"logid": 246757636,
"error_code": "0"
}

五、点击签到按钮后:

POST http://c.tieba.baidu.com/c/c/forum/sign HTTP/1.1
Charset: UTF-8
Content-Type: application/x-www-form-urlencoded
Accept-Encoding: gzip
User-Agent: Dalvik/1.6.0 (Linux; U; Android 4.1.2; GT-I9305 Build/JZO54K)
Host: c.tieba.baidu.com
Connection: Keep-Alive
Content-Length: 651

BDUSS=VucDlIREJHWnN6dU5xcjloalJwUHhZRUZjRG0tRkVaUDRldkdUYzM0bUtOQXhVQVFBQUFBJCQAAAAAAAAAAAEAAAA9nC4BY29kZW1vb24AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIqn5FOEp-RTSl%7C62606e0bb98f50caa59619508d4bdac3&_client_id=wappc_1407061932425_634&_client_type=2&_client_version=4.2.7&_is_mini=1&_phone_imei=354720056708957&cuid=89A7270749464157876426AB985C394E%7C759807650027453&fid=24188&from=xy0038&kw=%E7%BD%91%E7%BB%9C%E5%B0%8F%E8%AF%B4&model=GT-I9305&net_type=3&stErrorNums=0&stMethod=2&stMode=1&stSize=2443&stTime=166&stTimesNum=1&tbs=a6c5bc80231fc4811407078680&timestamp=1407078679178&sign=B2CD0A93CA8A243CD2D01578FC34A57F

Form这样好看点:

BDUSS VucDlIREJHWnN6dU5xcjloalJwUHhZRUZjRG0tRkVaUDRldkdUYzM0bUtOQXhVQVFBQUFBJCQAAAAAAAAAAAEAAAA9nC4BY29kZW1vb24AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIqn5FOEp-RTSl%7C62606e0bb98f50caa59619508d4bdac3
_client_id wappc_1407061932425_634
_client_type 2
_client_version 4.2.7
_is_mini 1
_phone_imei 354720056708957
cuid 89A7270749464157876426AB985C394E|759807650027453
fid 24188 //从上面第四步取出的id号
from xy0038
kw 网络小说 //从上面第四步取出的name
model GT-I9305
net_type 3
stErrorNums 0
stMethod 2
stMode 1
stSize 2443
stTime 166
stTimesNum 1
tbs a6c5bc80231fc4811407078680
timestamp 1407078679178
sign B2CD0A93CA8A243CD2D01578FC34A57F

签到成功返回JSON:

{
"user_info": {
"user_id": "19831869",
"is_sign_in": "1",
"user_sign_rank": "1846",
"sign_time": "1407506684",
"cont_sign_num": "1",
"total_sign_num": "186",
"cout_total_sing_num": "186",
"hun_sign_num": "67",
"total_resign_num": "0",
"is_org_name": "0",
"sign_bonus_point": "6",
"miss_sign_num": "5",
"level_name": "\u897f\u95e8\u5e86\u3001",
"levelup_score": "2000"
},
"server_time": "343169",
"time": 1407506683,
"ctime": 0,
"logid": 283907052,
"error_code": "0"
}

失败的话会返回失败原因,比如已签过了:

{
"error_code": "160002",
"error_msg": "\u4eb2\uff0c\u4f60\u4e4b\u524d\u5df2\u7ecf\u7b7e\u8fc7\u4e86",
"info": [],
"time": 1408628883,
"ctime": 0,
"logid": 2883284240,
"server_time": "131843"
}

error_msg的值,大家一眼就看穿是Unicode了吧,用站长工具在线翻译一下就知道它啥意思了

上面全部是废话吧?!都没说怎么做自动签到工具!其实不然,我只是个新手,简陋的工具我是用Qt做出来了,自用不敢献丑,免得授人以愚!下面只大概说说流程。

六、Qt描述

假设你已经通过各种手段(当然是注册啦!)取得用户名和密码,我们来模拟登陆:

void UserInfo::requestLogin() {
QMap<QString, QString> params;
params.insert("_client_id", client_id); //cliend_id似乎不是必需
params.insert("un", username); //用户名
params.insert("passwd", password.toAscii().toBase64()); //把密码base64编码

QUrl url("http://c.tieba.baidu.com/c/s/login");
QNetworkReply *reply = setRequest(params, url); //函数见下面
connect(reply, SIGNAL(finished()), this, SLOT(requestLoginFinish())); //槽函数见下下面
}

下面这个函数我用来给每个POST请求都调用一次,因为每个请求的HTTP Header都是一样的,sign值都是同样的计算方法。

//每个请求都相同的Header信息及参数urlEncode化计算sign值
QNetworkReply* UserInfo::setRequest(QMap<QString, QString> &params, QUrl url) {
qint64 time = QDateTime::currentMSecsSinceEpoch();
params.insert("timestamp", QString::number(time));

QByteArray postData; //用作请求
QByteArray calcSign; //用作计算Sign值
QMap<QString, QString>::iterator it;
for(it = params.begin(); it != params.end(); ++it){
postData.append(it.key());
postData.append("=");
postData.append(QUrl::toPercentEncoding(it.value()));
postData.append("&");

calcSign.append(it.key());
calcSign.append("=");
calcSign.append(it.value());
}
calcSign.append("tiebaclient!!!");
QString sign = QCryptographicHash::hash(calcSign, QCryptographicHash::Md5).toHex().toUpper();
postData.append("sign=").append(sign);

QNetworkRequest *request = new QNetworkRequest(url);
request->setRawHeader("Charset", "UTF-8");
request->setRawHeader("Content-Type", "application/x-www-form-urlencoded");
request->setRawHeader("Accept-Encoding", "gzip");
request->setRawHeader("User-Agent", "Dalvik/1.6.0 (Linux; U; Android 4.1.2; GT-I9305 Build/JZO54K)");
request->setRawHeader("Host", "c.tieba.baidu.com");
request->setRawHeader("Connection", "Keep-Alive");
request->setRawHeader("Content-Length", QByteArray::number(postData.length()));
//qDebug() << "Post:===>" << postData;
//namgr是QNetworkAccessManager
QNetworkReply *reply = namgr->post(*request, postData);
return reply;
}

收到服务器返回的JSON,处理内容:

void UserInfo::requestLoginFinish() {
QNetworkReply *reply = qobject_cast<QNetworkReply*>(sender());
//从网上抄来的GZIP解压函数,找遍了Qt竟然没有GZIP解压的函数
QByteArray json = unGzip(reply);
//qDebug() << endl << "Login Json:" << json;

//substr是自己写的用来提取字符串的
baiduss = substr(json, "\"BDUSS\":\"", "\"");
uid = substr(json, "\"id\":\"", "\"");
tbs = substr(json, "\"tbs\":\"", "\"");

if(!baiduss.isEmpty() && baiduss.length() > 10){
qDebug() << "BaiduSS:" << baiduss ;
qDebug() << "uid:" << uid;
qDebug() << "tbs:" << tbs;
loginStateMsg = tr("登陆成功");
}else{
qDebug() << "Login failed!" ;
QString errMsg = substr(json, "\"error_msg\":\"", "\"");
unicodeToString(errMsg);
loginStateMsg = errMsg;
qDebug() << errMsg;
}

if(isLogined()) { //记录一下登陆成功的时间
loginDate = QDate::currentDate();
requestFavList(); //登陆成功了就去取喜欢的贴吧
}
//让ui显示登陆返回的信息
emit loginStateChange(username, QVariant(loginStateMsg));

reply->deleteLater();
}

登陆成功后,读取喜欢的贴吧:

void UserInfo::requestFavList() {
QMap<QString, QString> params;
params.insert("_client_id", client_id);
params.insert("BDUSS", baiduss);

QUrl url("http://c.tieba.baidu.com/c/f/forum/favocommend");
QNetworkReply *reply = setRequest(params, url);
connect(reply, SIGNAL(finished()), this, SLOT(requestFavListFinish()));
}

收到服务器返回的JSON,处理内容:

void UserInfo::requestFavListFinish() {
QNetworkReply *reply = qobject_cast<QNetworkReply*>(sender());
QByteArray json = unGzip(reply);
//qDebug() << "fav json:" << json;

//获取服务器时间,因为签到是按服务器时间为准的嘛
if(serverTime.isEmpty()){
serverTime = substr(json, "\"time\":", ",").append("000");
emit serverTimeFetch(serverTime);
}
QString js(json);
unicodeToString(js);
bool hasBar = getBarsFromJson(js); //从Json里提取吧名和id

reply->deleteLater();
if(hasBar) requestSignIn(); //得到吧名和id就准备签到
}

提取所有吧名和id,放 QMap bars 里:

bool UserInfo::getBarsFromJson(QString &str){
QRegExp regex("\"name\":\"(.+)\",\"id\":([\\d]+),");
regex.setMinimal(true); //懒惰匹配模式
int pos = 0;
while((pos = regex.indexIn(str, pos)) != -1){
bars.insert(regex.cap(1), regex.cap(2));
pos += regex.matchedLength();
}
if (bars.size() > 0) return true;
return false;
}

发送POST请求模拟签到,参数很多不是必需的,自行测试吧,我懒,就都附上了:

void UserInfo::requestSignIn() {
QMap<QString, QString> params;
params.insert("_client_type", "2");
params.insert("_client_version", "4.2.7");
params.insert("_is_mini", "1");
params.insert("_os_version", "4.1.2");
params.insert("_phone_imei", "354720056708957");
params.insert("_phone_screen", "720,1280");
params.insert("cuid", "89A7270749464157876426AB985C394E|759807650027453");
params.insert("from", "xy0038");
params.insert("model", "GT-I9305");
params.insert("net_type", "3");
params.insert("stErrorNums", "0");
params.insert("_client_id", client_id);
params.insert("BDUSS", baiduss);
params.insert("tbs", tbs);

QUrl url("http://c.tieba.baidu.com/c/c/forum/sign");
QMapIterator<QString, QString> i(bars);
while(i.hasNext()){ //每个吧都单独POST请求
i.next();
params.insert("kw", i.key());
params.insert("fid", i.value());
QNetworkReply *reply = setRequest(params, url);
connect(reply, SIGNAL(finished()), this, SLOT(requestSignInFinish()));
}
}

输出签到结果,我就不提取了,各位要UI的就自行详细处理吧。

void UserInfo::requestSignInFinish() {
QNetworkReply *reply = qobject_cast<QNetworkReply*>(sender());
QString json = unGzip(reply);
unicodeToString(json);
qDebug() << endl << "sign result:" << json;
reply->deleteLater();
}

下面附上GZIP解压的代码,注意要把zlib的源代码导入Project里,还要#include "zlib.h"

//处理前检查一下是否是GZIP编码(及Magic code,好像有点多余)
QByteArray UserInfo::unGzip(QNetworkReply *reply) {
QByteArray content = reply->readAll();
if(reply->hasRawHeader("Content-Encoding") &&
reply->rawHeader("Content-Encoding")=="gzip" &&
(unsigned char)content.at(0) == 0x1F &&
(unsigned char)content.at(1) == 0x8B){
content = decompressGZIP(content); //正主
}
return content;
}
//GZIP解压代码来自: http://blog.sina.com.cn/s/blog_de7eec9c0101bo5u.html
QByteArray UserInfo::decompressGZIP(QByteArray srcData) {
z_stream ungzipstream;
unsigned char buffer[4096];
int ret = 0;
QByteArray unComprData;

ungzipstream.zalloc = Z_NULL;
ungzipstream.zfree = Z_NULL;
ungzipstream.opaque = Z_NULL;
ungzipstream.avail_in = srcData.size();
ungzipstream.next_in = (Bytef*)srcData.data();
ret = inflateInit2(&ungzipstream,47);
if(ret != Z_OK)
return QByteArray();
do {
memset(buffer,0,4096);
ungzipstream.avail_out = 4096;
ungzipstream.next_out = buffer;
ret = inflate(&ungzipstream,Z_NO_FLUSH);
switch(ret)
{
case Z_NEED_DICT:
ret = Z_DATA_ERROR;
case Z_DATA_ERROR:
case Z_MEM_ERROR:
return QByteArray();
}

if(ret != Z_FINISH)
{
unComprData.append((char*)buffer);
}
}while(ungzipstream.avail_out == 0);
return unComprData;
}

如果要弄成每天自动签到,注意baiduss有效期可能是一个月,可能是半个月或几天,我没有验证。所以不用每次都登陆,隔些天登陆一次就好了。

void UserInfo::startLogin(){
if(isSessionExpired())
requestLogin();
else
requestFavList();
}

//登陆是否过期,假设baiduss超过6天就过期
bool UserInfo::isSessionExpired(){
//还记得前面的loginDate么?
int expire = loginDate.daysTo(QDate::currentDate());
if(expire > 6) {
baiduss.clear();
return true;
}
return false;
}

好了,就这么多了,掏不出什么东西给大家了,上面内容基本够实现自动登陆+签到了。

当然,如果你像我一样不满足于随便签到,要抢签到前十名,那就要下点功夫改进改进,以服务器时间为准,检测是否到23:59:58,到了就一秒N个Post请求不断发给服务器……嘿嘿,我会告诉你我后来签了D吧第二吗?

写得好累,在这里的注释比我代码里的注释还多……希望对大家有所启发。