码迷,mamicode.com
首页 > 编程语言 > 详细

qq聊天机器人 群发工具 (java版) (一)

时间:2015-05-27 22:56:41      阅读:728      评论:0      收藏:0      [点我收藏+]

标签:webqq   qq机器人   qq群发工具   java   

这是最近因为感兴趣才写的小东西,网上大多是易语言版,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.com/check?pt_tea=1&uin=2368295990&appid=501004106&js_ver=10124&js_type=0&login_sig=&u1=http%3A%2F%2Fw.qq.com%2Fproxy.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.com/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<Cookie> cookies = ((AbstractHttpClient) client).getCookieStore()
			.getCookies();
	HashMap<String, String> map = new HashMap<String, String>();
	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<String, String>) response[2]).get("ptvfsession");
如果需要验证码登陆,则在获取图片返回的cookie中提取,方法同上,即在发送请求后获取cookie,然后获取指定字段的值。
2.第一次登陆

Request URL:https://ssl.ptlogin2.qq.com/login?u=3277086849&p=nRjIiseX0-13YK3Oc5IBhwkIdogP3pBAdO0FxhVCVxZ49bom8qvdrKvrSLQ0EVw*vUIgBQZIELIR18cW*q5nv9ZR4vSXIWkO99LChJvug3DiJiAsJ5iB9zvZuVHwD7lSvCY8dGvsaodgjQf0WG0iZApVevfsGIWzCuQ7xsdRgezjrYPXCueJB5oFekAKREHbX9csjq5zVbuAsd8jqPNvew__&verifycode=uwno&webqq_type=10&remember_uin=1&login2qq=1&aid=501004106&u1=http%3A%2F%2Fw.qq.com%2Fproxy.html%3Flogin2qq%3D1%26webqq_type%3D10&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.com/check_sig?pttype=1&uin=3277086849&service=login&nodirect=0&ptsigx=a8f07aea84f23d76363c625b3aa1da48fbb21288078e3229e331955dd64727440da4b0892214efa457a69685a910a2592fb29aa6896121bfbcac6e0bf88dff1e&s_url=http%3A%2F%2Fw.qq.com%2Fproxy.html%3Flogin2qq%3D1%26webqq_type%3D10&f_url=&ptlang=2052&ptredirect=100&aid=501004106&daid=164&j_later=0&low_login_hour=0&regmaster=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.com/check_sig?pttype=1&uin=3277086849&service=login&nodirect=0&ptsigx=a8f07aea84f23d76363c625b3aa1da48fbb21288078e3229e331955dd64727440da4b0892214efa457a69685a910a2592fb29aa6896121bfbcac6e0bf88dff1e&s_url=http%3A%2F%2Fw.qq.com%2Fproxy.html%3Flogin2qq%3D1%26webqq_type%3D10&f_url=&ptlang=2052&ptredirect=100&aid=501004106&daid=164&j_later=0&low_login_hour=0&regmaster=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.com/api/getvfwebqq?ptwebqq=9d37ec0c729300bb5dbd927c917f0e851794662e9164c2e6fadab64cea4f6208&clientid=53999199&psessionid=&t=1432729913114
Request Method:GET
Referer:http://s.web2.qq.com/proxy.html?v=20130916001&callback=1&id=1

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

httpGet.setHeader("Referer","http://s.web2.qq.com/proxy.html?v=20130916001&callback=1&id=1");
返回值为json格式,解析后直接获取ptwebqq。

{"retcode":0,"result":{"vfwebqq":"a57e6b8ea3409910fb139d5949b850e47879e71f133b6aa5e7c593c1724343ae8789f9f0c5286f91"}}
5.第二步登陆

Request URL:http://d.web2.qq.com/channel/login2
Request Method:POST
Content-Type:application/x-www-form-urlencoded
Referer:http://d.web2.qq.com/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.com/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.com/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聊天机器人 群发工具 (java版) (一)

标签:webqq   qq机器人   qq群发工具   java   

原文地址:http://blog.csdn.net/u013401219/article/details/46050557

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!