这是最近因为感兴趣才写的小东西,网上大多是易语言版,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,然后获取指定字段的值。
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®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.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®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.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了。之后有时间会再介绍群发消息和自动回复功能。
原文地址:http://blog.csdn.net/u013401219/article/details/46050557