admin管理员组

文章数量:1559070

这是最近因为感兴趣才写的小东西,网上大多是易语言版或c #版,java仅有的版本也偏老,老版webqq协议早失效了,所以现在我写了一个最新版本的。要实现群发和自动回复消息以及更多自定义功能,首先要实现登陆qq,这边主要介绍一下如何分析qq协议以及如何登陆。

我并没有使用很专业的抓包工具,事实上现在的浏览器一般都能查看到get,post请求的主要内容,而我们所需要的也就是请求的内容和地址,所以一个360浏览器或者google浏览器足够我们分析了。

首先分析流程,再讲方法。第一步登陆webqq的网站,我们会看到登陆界面,打开f12,每隔一段时间会执行一个请求,大概是判断左侧二维码是否失效的方法吧,不过这个与登陆无关,直接忽略。


填写完账号,失去输入框的焦点后,又会触发一个请求,返回了一串字符串,返回的字符串可能是执行某个js方法吧(大概,我也不清楚),不过这也并不重要,先看图:

这个请求是为了判断该账号是否需要填写图片验证码才能登陆。不看外面的方法名,里面第一个参数0则代表不需要填写图片验证码登陆,第二个参数要记下,相当于登陆时要用的验证码(不过它不同于图片验证码,如果需要图片验证码则还需要发个请求来获取图片,然后根据图片中的字母来填写验证码,而此处相当于省略了这一段环节,可以理解为服务器直接告诉你了验证码是什么,下面会介绍需要图片验证码的流程),第三个参数为你账号的十六进制值,不过与登陆环节无关吧,最后2个参数有什么用我也不知道,不过与主要登陆环节无关。

下图为需要验证码登陆返回的数据,第一个参数为1代表需要验证码(这时就要再发一个请求来获取验证码图片了),第二个参数要记下,等下要作为获取图片验证码请求的参数传递到服务器。


这时就需要发送一个请求来获取图片验证码了,如下图:


获取到验证码后就该填写密码登陆了,只不过登陆也分为几步,首先第一次登陆,第一个参数返回是否成功,0即成功可以往下执行,4即验证码错误,3即账号密码错误。第3个参数为成功后的回调方法,也即你在第一步登陆成功后紧接着要发送的请求。如果第一步登陆失败,是不会有回调方法的,第三个参数返回的是0吧,记不清了。


如果第一步登陆成功,紧接着就要发送下一个请求,请求的地址为第一步登陆成功后返回的第3个参数(url链接)。这一步是必须的,要更新cookie(后续介绍),不然第二步登陆肯定失败。

这一步没什么返回结果,只是用来更新cookie,来进行第二步登陆。

接下来要获取一个参数vfwebqq,这个参数与登陆无关,但你要获取qq好友列表和群列表时必须要带上(注意:第二步登陆也会返回这个参数,但与这个请求获取到的vfwebqq不同,但是真正获取好友列表和群列表的参数是这一步获取到的,可能是最近更新的结果)。


接下来就是最后一步了,第二步登陆,如果返回成功,则已登陆的qq会被挤下线,第一个参数为0即代表登陆成功(在qq登陆的过程中,返回的json数据里retcode基本代表返回结果,0即成功),返回的参数在后续的方法中介绍。


总结一下登陆的过程即:

1.判断是否需要验证码登陆,若需要则先获取验证码图片。

2.填写表单信息完成后进行第一次登陆。

3.执行第一步登陆成功后的回调方法(发送请求)。

4.获取vfwebqq。(如果只想做一个自动回复的机器人的话,这个参数是不需要的,但如果要做群发软件则需要获取好友列表和群列表,则必须要获取这个参数;至于为什么这一步放在第一步登陆与第二步中间是因为网页qq里是按这个顺序发送请求的,改变顺序会有什么影响我不知道,但是至少不会影响第二次登陆)。

5.第二步登陆,若成功即登陆成功。

以上只是流程分析,若以后webqq协议再变化,理论上是可以按照此过程去抓包再分析的,应该也就是改改参数,改改方法链接上的小问题,至少13年到现在流程上是没什么太大的变化。


下面介绍java实现的方法,整个通信过程都是通过发送http请求实现的,我看网上介绍的方法大多是自己封装httpurlconnection类发送http请求的,不过我都是用的defaulthttpclient这个类,需要引用额外的jar包。我只以我写方法做介绍吧。

1.检测是否需要验证码

request url:https://ssl.ptlogin2.qq/check?pt_tea=1&uin=2368295990&appid=501004106&js_ver=10124&js_type=0&login_sig=&u1=http://w.qq/proxy.html&r=0.7602819891180843
request method:get
此处为get请求,除了uin要设置为自己的账号之外,其他参数都可不变,有些是固定的参数,有些是随机的参数,最保险的是都不变化....

返回字符串为,如果不需要验证码,则记录第二个参数(即!kbk,每次访问获取的值都不一样)。

ptui_checkvc('0','!kbk','\x00\x00\x00\x00\x8d\x29\x54\x36','2c6bf125c7708d33cc7c9bea99a9b5f6fd7e7d3f3f8e676cbff24bb0845de8cefaf75b88ea5efd5d36caa2d34b997d0ee8c7b638757af32a','0');
若需要验证码,即第一个参数返回1,则记录第二个参数,作为下次请求发送的参数(即jfmgyuolfstyrbtmi9i2ui8apugejxo4a1l3oaj1ksu3vkqfdz8w8g**)
ptui_checkvc('1','jfmgyuolfstyrbtmi9i2ui8apugejxo4a1l3oaj1ksu3vkqfdz8w8g**','\x00\x00\x00\x00\xc3\x54\x60\x81','','0');

发送请求获取验证码图片:

request url:https://ssl.captcha.qq/getimage?aid=501004106&r=0.008850367739796638&uin=3277086849&cap_cd=jfmgyuolfstyrbtmi9i2ui8apugejxo4a1l3oaj1ksu3vkqfdz8w8g**
request method:get
请求依旧为get,uin为账号,cap_cd为刚才的参数。此时返回的是图片的二进制数据流,再就看如何处理图片了,我先附上我处理图片的方法,其实就是将数据流写进图片文件,保存在本地,再显示到界面上。验证码的像素一般是130*53,zoominimage(path)是为了缩小图片,方便显示到界面,不然在前台改变分辨率图片总是显示不全。因为我不太会处理swing界面的图片,所以处理手法比较拙劣,直接忽略吧。
/**
 * @title 根据二进制字符串生成图片
 * @param data
 *            生成图片的二进制字符串
 * @param filename
 *            图片名称(完整路径)
 * @param type
 *            图片类型
 * @return
 */
public static void saveimage(string data, string filename, string type) {
	bufferedimage image = new bufferedimage(130, 53,
			bufferedimage.type_byte_binary);
	bytearrayoutputstream byteoutputstream = new bytearrayoutputstream();
	try {
		imageio.write(image, type, byteoutputstream);
		byte[] bytes = hex2byte(data);
		string path = system.getproperty("user.dir")   "/resources/img/"
				  filename;
		string respath = system.getproperty("user.dir")
				  "/resources/img/temp.jpg";
		randomaccessfile file = new randomaccessfile(path, "rw");
		file.write(bytes);
		file.close();
		zoominimage(path);// 缩小图片
	} catch (ioexception e) {
		e.printstacktrace();
	}
}
/**
 * 反格式化byte
 * 
 * @param s
 * @return
 */
public static byte[] hex2byte(string s) {
	byte[] src = s.tolowercase().getbytes();
	byte[] ret = new byte[src.length / 2];
	for (int i = 0; i < src.length; i  = 2) {
		byte hi = src[i];
		byte low = src[i   1];
		hi = (byte) ((hi >= 'a' && hi <= 'f') ? 0x0a   (hi - 'a')
				: hi - '0');
		low = (byte) ((low >= 'a' && low <= 'f') ? 0x0a   (low - 'a')
				: low - '0');
		ret[i / 2] = (byte) (hi << 4 | low);
	}
	return ret;
}
再附上发送http请求的方法:
private static cookiestore cs = null;// 存储最近一次的cookie 下次发送http请求的时候带上此cookie
// 接收消息和cookie
public static object[] gethttpdataandcookie(string url) throws exception {
	defaulthttpclient client = new defaulthttpclient();
	httpget httpget = new httpget(url);
	httpclientparams.setcookiepolicy(client.getparams(),
			cookiepolicy.browser_compatibility);
	// 设置cookiestore
	if (cs != null) {
		client.setcookiestore(cs);
	}
	httpresponse httpresponse = client.execute(httpget);
	// 保存cookiestore
	cs = client.getcookiestore();
	httpentity httpent = httpresponse.getentity();
	string code = string.valueof(httpresponse.getstatusline()
			.getstatuscode());
	string line;
	stringbuffer sb = new stringbuffer();
	// 获取cookie
	list cookies = ((abstracthttpclient) client).getcookiestore()
			.getcookies();
	hashmap map = new hashmap();
	if (!cookies.isempty()) {
		for (int i = 0; i < cookies.size(); i  ) {
			map.put(cookies.get(i).getname(), cookies.get(i).getvalue());
		}
	}
	if (httpent != null) {
		bufferedreader br = new bufferedreader(new inputstreamreader(
				httpent.getcontent(), "utf-8"));
		while ((line = br.readline()) != null) {
			sb.append(line);
		}
		br.close();
	}
	return new object[] { sb.tostring(), code, map };
}
这里一定要注意cs这个参数,即最近一次请求所获取到的cookie,下次发送请求时一定要带上!前面几步可能不会出错,但后面几步一直不成功就有可能是cookie没综合的原因(当初我卡在这里好久)。map是我将返回的cookie做了处理转成了hashmap,是因为有些地方存储返回的cookie字段,作为下次请求的参数。在第一步整体过程中,需要获取一个参数 ptvfsession。如果不需要验证码登陆,则在检测验证码返回的cookie中提取出这个参数,如下:
ptvfsession = ((hashmap) response[2]).get("ptvfsession");
如果需要验证码登陆,则在获取图片返回的cookie中提取,方法同上,即在发送请求后获取cookie,然后获取指定字段的值。
2.第一次登陆
request url:https://ssl.ptlogin2.qq/login?u=3277086849&p=nrjiisex0-13yk3oc5ibhwkidogp3pbado0fxhvcvxz49bom8qvdrkvrslq0evw*vuigbqzielir18cw*q5nv9zr4vsxiwko99lchjvug3dijiasj5ib9zvzuvhwd7lsvcy8dgvsaodgjqf0wg0izapvevfsgiwzcuq7xsdrgezjrypxcuejb5ofekakrehbx9csjq5zvbuasd8jqpnvew__&verifycode=uwno&webqq_type=10&remember_uin=1&login2qq=1&aid=501004106&u1=http://w.qq/proxy.html?login2qq=1&webqq_type=10&h=1&ptredirect=0&ptlang=2052&daid=164&from_ui=1&pttype=1&dumy=&fp=loginerroralert&action=0-21-1678643&mibao_css=m_webqq&t=1&g=1&js_type=0&js_ver=10124&login_sig=&pt_randsalt=0&pt_vcode_v1=0&pt_verifysession_v1=h02b7ejxn9dccz7wqzlvnwbqweqlvaygwimcvohr2v5zchm7iphi63jnnsdf3o5gq7serlwot_cd_ijonlbizh3wypecsvauno1
request method:get
依旧是get请求,u即账号,p即加密后的密码(等下介绍如何加密),verifycode即验证码(4位,如果有图片验证码,则填写图片中的字母,不需要则填写之前返回的值,即!开头的4位),中间一串参数可以不变化,结果的参数ptvfsession为第一步我们获取到的ptvfsession。
这一步返回的字符串为:
ptuicb('0','0','http://ptlogin4.web2.qq/check_sig?pttype=1&uin=3277086849&service=login&nodirect=0&ptsigx=a8f07aea84f23d76363c625b3aa1da48fbb21288078e3229e331955dd64727440da4b0892214efa457a69685a910a2592fb29aa6896121bfbcac6e0bf88dff1e&s_url=http://w.qq/proxy.html?login2qq=1&webqq_type=10&f_url=&ptlang=2052&ptredirect=100&aid=501004106&daid=164&j_later=0&low_login_hour=0®master=0&pt_login_type=1&pt_aid=0&pt_aaid=0&pt_light=0','0','登录成功!', 'scumvirus');
0即成功,第3个参数为回调方法的url地址。

这一步依旧要根据返回的cookie获取一个参数——ptwebqq

下面介绍如何加密密码,加密方法是在js上实现的,之后附上js文件(可能隔一段时间会变化一次,但是总有大神会破解的吧)。我是通过直接调用js里的getencryption()方法来加密密码的,附上调用js的方法:

/**
 * 执行js函数,得到需要的值的值
 * 
 * @param paras
 * @return
 * @throws scriptexception
 * @throws filenotfoundexception
 * @throws nosuchmethodexception
 */
public static string mdp(string p, string account, string code) {
	object t = null;
	try {
		scriptenginemanager m = new scriptenginemanager();
		scriptengine se = m.getenginebyname("javascript");
		se.eval(new filereader(new file("resources/js/qqrsa.js")));
		t = se.eval("getencryption(\""   p   "\",\""   account   "\",\""
				  code   "\")");
		return t.tostring();
	} catch (exception e) {
		e.printstacktrace();
	}
	return t.tostring();
}
其中p为未加密的密码,account即qq账号,code是verifycode,即验证码,同上。返回的字符串即加密后的密码。附上js文件下载地址: qqrsa.js

3.执行回调方法更新cookie

request url:http://ptlogin4.web2.qq/check_sig?pttype=1&uin=3277086849&service=login&nodirect=0&ptsigx=a8f07aea84f23d76363c625b3aa1da48fbb21288078e3229e331955dd64727440da4b0892214efa457a69685a910a2592fb29aa6896121bfbcac6e0bf88dff1e&s_url=http://w.qq/proxy.html?login2qq=1&webqq_type=10&f_url=&ptlang=2052&ptredirect=100&aid=501004106&daid=164&j_later=0&low_login_hour=0®master=0&pt_login_type=1&pt_aid=0&pt_aaid=0&pt_light=0
request method:get
还是get方法,url地址即为第一步登陆返回的地址,不需要记录任何参数,只要别忘了更新一遍cookie即可。

4.获取vfwebqq,姑且写在这,免得之后再介绍

request url:http://s.web2.qq/api/getvfwebqq?ptwebqq=9d37ec0c729300bb5dbd927c917f0e851794662e9164c2e6fadab64cea4f6208&clientid=53999199&psessionid=&t=1432729913114
request method:get
referer:http://s.web2.qq/proxy.html?v=20130916001&callback=1&id=1

方法依旧是get,ptwebqq第一步登陆获取到,clientid为8-9位任意值,psessionid为空,t为当前时间戳。不过此处要多加一个request头:

httpget.setheader("referer","http://s.web2.qq/proxy.html?v=20130916001&callback=1&id=1");
返回值为json格式,解析后直接获取ptwebqq。
{"retcode":0,"result":{"vfwebqq":"a57e6b8ea3409910fb139d5949b850e47879e71f133b6aa5e7c593c1724343ae8789f9f0c5286f91"}}
5.第二步登陆
request url:http://d.web2.qq/channel/login2
request method:post
content-type:application/x-www-form-urlencoded
referer:http://d.web2.qq/proxy.html?v=20130916001&callback=1&id=2
form-data:r={"ptwebqq":"9d37ec0c729300bb5dbd927c917f0e851794662e9164c2e6fadab64cea4f6208","clientid":53999199,"psessionid":"","status":"online"}
post请求,url连接很简单,但是必须设置content-type为application/x-www-form-urlencoded,设置referer:http://d.web2.qq/proxy.html?v=20130916001&callback=1&id=2,附上我的代码:
public static synchronized string[] posthttpdata(string url, string data)
		throws exception {
	// post 请求
	defaulthttpclient client = new defaulthttpclient();
	httppost postjson = new httppost(url);
	postjson.setheader("referer",
			"http://d.web2.qq/proxy.html?v=20130916001&callback=1&id=2");
	httpclientparams.setcookiepolicy(client.getparams(),
			cookiepolicy.browser_compatibility);
	client.getparams().setparameter(
			coreconnectionpnames.connection_timeout, 5000);
	client.getparams().setparameter(coreconnectionpnames.so_timeout, 5000);
	stringentity entity = new stringentity(data);
	entity.setcontenttype("application/x-www-form-urlencoded");
	postjson.setentity(entity);
	// 设置cookiestore
	if (cs != null) {
		client.setcookiestore(cs);
	}
	// 获得返回的json数据包
	httpresponse httpresponse = client.execute(postjson);
	httpentity httpent = httpresponse.getentity();
	// 保存cookiestore
	cs = client.getcookiestore();
	string code = string.valueof(httpresponse.getstatusline()
			.getstatuscode());
	string line;
	stringbuffer sb = new stringbuffer();
	if (httpent != null) {
		bufferedreader br = new bufferedreader(new inputstreamreader(
				httpent.getcontent(), "utf-8"));
		while ((line = br.readline()) != null) {
			sb.append(line);
		}
		br.close();
	}
	return new string[] { sb.tostring(), code };
}
下面是post所带的参数:
string obj = "r={\"ptwebqq\":\""   ptwebqq   "\",\"clientid\":"  clientid   ",\"psessionid\":\"\",\"status\":\"online\"}";
其中"r="不能掉,不然不识别,不能登陆成功。前面3个参数前面都介绍了,最后一个参数即登陆状态,online即在线,hidden即隐身。
若登陆成功,则返回:
{"retcode":0,"result":{"uin":3277086849,"cip":1899593934,"index":1075,"port":53012,"status":"online","vfwebqq":"0f28abebb129b3e77be531d95640217425dfcb9b65faf36108f136239b4df2efdb69865776ba26f0","psessionid":"8368046764001d636f6e6e7365727665725f77656271714031302e3133392e372e313630000012a800000aef026e0400816054c36d0000000a404256456156376766746d000000280f28abebb129b3e77be531d95640217425dfcb9b65faf36108f136239b4df2efdb69865776ba26f0","user_state":0,"f":0}}
其中只需记录下 psessionid,以后发送消息要用到。

若最后一步成功,那么你已经成功登陆qq了。之后有时间会再介绍群发消息和自动回复功能。









本文标签: 机器人qq