Web微信模拟

一、概要

目的:实现一个具有web微信类似功能的项目

框架:Django

模块:render、HttpResponse、BeautifulSoup、re、time、requests、json、random

特点:web微信和其他的不太一样,这里不需要账号和密码,只需要扫描网页提供的二维码即可

二、具体步骤

1、登录页面

  既然是要实现web版的微信,那么我们就要知道web微信都干了些什么。打开一个网页,右键点击检查,在地址栏输入web微信(https://wx.qq.com/)回车,我们会看到一个等待扫描的二维码页面。我们先来看一下这个二维码是如何来的,我们会看到二维码的标签有个src="https://login.weixin.qq.com/qrcode/oc8PLqKx0w==", 因为每次请求的时候二维码都会变化,我们猜测这个src中最后一个'/'后面的值是变化的,我们再去Network中去找到这个返回值。检查后我们会发现一个请求名为jsloginappid=wx782c26e4c19acffb&redirect_uri=https%3A%2F%2Fwx.qq.com%2Fcgi-bin%2Fmmwebwxbin%2Fwebwxnewloginpage&fun=new&lang=zh_CN&_=1487297537475的response中有个uuid的值和我们需要的值类似。我们把这个求情的URL保存下来:https://login.wx.qq.com/jslogin?appid=wx782c26e4c19acffb&redirect_uri=https%3A%2F%2Fwx.qq.com%2Fcgi-bin%2Fmmwebwx-bin%2Fwebwxnewloginpage&fun=new&lang=zh_CN&_=1487297850694,这个请求的方式是"GET"。观察后发现这个URL里的大部分的参数都是状态值,只有一个'_'我们猜测是时间戳。现在我们就可以试试能不能获取到二维码。

代码:

 1 <!DOCTYPE html>
 2 <html lang="en">
 3 <head>
 4     <meta charset="UTF-8">
 5     <title>Title</title>
 6 </head>
 7 <body>
 8     <div style="width: 300px; margin: 0 auto">
 9         <!--二维码路径-->
10         <img src="https://login.weixin.qq.com/qrcode/{{ code }}">
11     </div>
12     <!--注释掉的部分是稍后请求扫码状态的函数-->
13     <!--<script src="/static/jquery-3.1.1.js"></script>
14     <script>
15         $(function () {
16             polling();
17         });
18         function polling() {
19             $.ajax({
20                 url: '/long_polling/',
21                 type: 'GET',
22                 dataType: 'json',
23                 success: function (arg) {
24                     if (arg.status == 408){
25                         polling()
26                     }else if (arg.status == 201){
27                         console.log(123);
28                         $('img').attr('src', arg.data);
29                         polling()
30                     }else {
31                         location.href = '/index/'
32                     }
33                 }
34             })
35         }
36     </script>-->
37 </body>
38 </html>
login.html
 1 from django.shortcuts import render
 2 
 3 from django.shortcuts import HttpResponse
 4 
 5 from bs4 import BeautifulSoup
 6 
 7 import re
 8 
 9 import time
10 
11 import requests
12 
13 import json
14 
15 import random
16 
17 CURRENT_TIME = None
18 QCODE = None
19 LOGIN_COOKIE_DICT = {}
20 TICKET_COOKIE_DICT = {}
21 TICKET_DICT = {}
22 TIPS = 1
23 BASE_URL = ''
24 BASE_SYNC_URL = ''
25 USER_ID = ''
26 USER_INFO = {}
27 USER_LIST_DIC = {}
28 #  这里用不到的全局变量后面会用到
29 
30 
31 def login(request):
32     # 登录页面,显示登录的二维码
33     base_qcode_url = 'https://login.wx.qq.com/jslogin?appid=wx782c26e4c19acffb&redirect_uri=https%3A%2F%2Fwx.qq.com%' \
34                      '2Fcgi-bin%2Fmmwebwx-bin%2Fwebwxnewloginpage&fun=new&lang=zh_CN&_={0}'
35     global CURRENT_TIME
36     CURRENT_TIME = str(time.time())
37     q_code_url = base_qcode_url.format(CURRENT_TIME)
38     respons = requests.get(q_code_url)
39     # 二维码后缀
40     global QCODE
41     QCODE = re.findall('uuid = "(.*)";', respons.text)[0]  # 拿括号里的内容的列表
42 
43     return render(request, 'login.html', {'code': QCODE})
Views 获取二维码函数

  这样我们可以看到一个二维码界面。接下来分析web微信做了什么:先给我们一个二维码,等待我们扫描,我们扫描后二维码会变成我们的头像,在手机端点击确认之后页面刷新,登录成功。

2、扫描并确认登录

  我们扫描的时候是手机端给微信服务器发送了一个确认的请求。然后微信服务器将这个状态返回到web。但我们知道HTTP是无状态的,那么服务器如何将状态发送给我们的,我们猜测会有一个请求一直在发送。观察几分钟,会发现每隔25s左右会有一个请求发送,请求的地址为:https://login.wx.qq.com/cgi-bin/mmwebwx-bin/login?loginicon=true&uuid=******&tip=0&r=******&_=******。我们看下这个请求的response,然后测试扫描和确认登录后这个返回值会不会有变化。当没有扫描二维码的时候返回值是window.code=408,扫描二维码之后是window.code=201,确认登录后是window.code=200。这个url里loginicon和tip是状态值,uuid我们猜测是刚才的二维码uuid,'_'是的值是一个时间戳,那么还剩下r我们没有值,检查之后我们发现并没有类似的返回值,我们先它作为一个随机值看,请求方式"GET",在请求的时候,直接将我们看到的数复制。然后我们去测试一下。在登录的HTML中我们在加载好页面之后执行一个类似于web等待扫描的长轮循函数,到views函数中去发送这个请求。我们将第一步HTML代码中注释掉的部分恢复。并在views中添加登录的代码。这里需要注意,在扫描或登录之后需要改变tip值为1,避免重复请求。确认登录之后我们将cookies进行保存。之后的请求中需要用到。

代码:

 1 def long_polling(request):
 2     ret = {'status': 408, 'data': None}
 3 
 4     try:
 5         global TIPS
 6         base_login_url = 'https://login.wx.qq.com/cgi-bin/mmwebwx-bin/login?loginicon=true&uuid={0}&tip={1}&' \
 7                          'r=-940286750&_={2}'
 8 
 9         login_url = base_login_url.format(QCODE, TIPS, CURRENT_TIME)
10 
11         response_login = requests.get(login_url)
12 
13         if 'window.code=201' in response_login.text:
14             TIPS = 0
15             avatar = re.findall("userAvatar = '(.*)';", response_login.text)
16             ret['status'] = 201
17             ret['data'] = avatar
18         elif 'window.code=200' in response_login.text:
19             ret['status'] = 200
20             # 扫码点击确认后获取cookie
21             LOGIN_COOKIE_DICT.update(response_login.cookies.get_dict())
22             # 获取redirect的url
23             base_ticket_url = re.findall('redirect_uri="(.*)";', response_login.text)[0]
24             # 不同的微信号在初始话数据的时候有不同的地址,需要甄别
25             global BASE_URL
26             global BASE_SYNC_URL
27             if base_ticket_url.startswith('https://wx2.qq.com'):
28                 BASE_URL = 'https://wx2.qq.com'
29                 BASE_SYNC_URL = 'https://webpush.wx2.qq.com'
30             else:
31                 BASE_URL = 'https://wx.qq.com'
32                 BASE_SYNC_URL = 'https://webpush.wx.qq.com'
33             # 组成获取票据的url
34             ticket_url = base_ticket_url + '&fun=new&version=v2&lang=zh_CN'
35             # 获取票据同时获取cookies
36 
37             response_ticket = requests.get(url=ticket_url, cookies=LOGIN_COOKIE_DICT)
38             TICKET_COOKIE_DICT.update(response_ticket.cookies.get_dict())
39             # 分析票据
40             soup = BeautifulSoup(response_ticket.text, 'html.parser')
41             for tag in soup.find():
42                 TICKET_DICT[tag.name] = tag.string
43     except Exception as e:
44         print(e)
45     return HttpResponse(json.dumps(ret))
Views 扫描登录函数

  我们在获取返回值的时候有一些在之后会用到,需要保存,并且web微信在确认登录后,会跳转页面,新页面会有两个,一个是:https://wx.qq.com/,另一个是:https://wx2.qq.com/。需要区别对待,如果这里不正确的话不能获取到信息。

  登录成功后需要获取用户的基本信息,以及最近联系人列表。这是我们下一步要做的,初始化用户数据。

3、初始化用户数据

   web微信在登录成功后会跳转一个页面,我们模仿这个方式,在确认登录之后,跳转URL,显示用户数据。我们再回到web微信检查Network看用户数据是哪个请求的response。可以找到一个webwxinit开头的请求,内部有初始化的数据。URL:https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxinit?r=****&pass_ticket=****,这个URL有的参数我们是没有的,那么就要看看在这个请求之前是否有其他请求返回这些数据。可以发现一个webwxnewloginpage开头的请求,它有一个票据的返回数据,正是我们需要的。拿到数据,获取票据的时候需要重新赋值一个cookies。后边会用到。获取票据的代码我们写在登录的那个函数中。使用BeautifulSoup重构数据。然后去请求用户数据初始化。然后将初始化的数据拿出展示在页面。初始化用户数据的时候用的是POST请求,数据这里需要通过这个请求去看需要发送什么样的数据,以及在headers里检查数据类型是什么类型。所以我们发送POST请求的时候,在数据这边,是以"json"为key的。数据中有一个设备ID,可以参考前几次请求的ID填写。另外几条都在webwxnewloginpage的response中。

代码:

  1 <!DOCTYPE html>
  2 <html lang="en">
  3 <head>
  4     <meta charset="UTF-8">
  5     <title>Title</title>
  6 </head>
  7 <body>
  8     <div>
  9         <h1>个人信息</h1>
 10         <a style="font-size: 20px; color: #1c5a9c">{{ info.User.NickName }}</a>
 11         <a id="from_user_id">{{ info.User.UserName }}</a>
 12         <p><input id="user_id" type="text" placeholder="请输入用户ID"></p>
 13         <p><input id="msg_content" type="text" placeholder="请输入内容"></p>
 14         <input id="send_msg" onclick="send_msg(this)" type="button" value="发送">
 15     </div>
 16     <div id="msg_box" style="height: 300px; width: 800px; border: solid 1px gray; overflow: auto">
 17 
 18     </div>
 19     <h1>最近联系人</h1>
 20     {% for item in info.ContactList %}
 21         <p>
 22             <a style="font-size: 20px; color: #2F72AB">{{ item.NickName }}</a><a>{{ item.UserName }}</a>
 23             <a>{{ item.Signature }}</a>
 24         </p>
 25     {% endfor %}
 26     <div>
 27         <div id="get_list" onclick="get_list()" style="cursor: pointer">获取全部好友</div>
 28         <div class="empty"></div>
 29     </div>
 30     <h1>公众号</h1>
 31     {% for item in info.MPSubscribeMsgList %}
 32         <p>
 33             <a style="font-size: 20px; color: #8a6d3b;">{{ item.NickName }}</a><a style="display: none">{{ item.UserName }}</a>
 34         </p>
 35         <p>
 36             {% for i in item.MPArticleList %}
 37                 <div>
 38                     <a style="font-size: 18px">{{ i.Title }}</a>
 39                     <a href="{{ i.Url }}">{{ i.Digest }}</a>
 40                 </div>
 41 
 42             {% endfor %}
 43         </p>
 44     {% endfor %}
 45 
 46 <!--注释的部分是在获取好友列表以及发送和接收消息的时候用到的-->
 47 <!--<script src="/static/jquery-3.1.1.js"></script>
 48 <script>
 49     <!--在页面加载好之后启动获取消息的函数-->
 50     $(function () {
 51        get_msg() 
 52     });
 53     <!--获取好友列表函数-->
 54     function get_list() {
 55         $.ajax({
 56             url: '/get_list',
 57             type: 'GET',
 58             dataType: 'json',
 59             success: function (arg) {
 60                 var list = $("#get_list").siblings()[0];
 61                 if ($(list).hasClass('empty')){
 62                     var tag = '';
 63                     for (var i in arg.MemberList){
 64                         tag += "<div><a>" + arg.MemberList[i].NickName + "</a><a>[" + arg.MemberList[i].UserName + "]</a><a>[" + arg.MemberList[i].Province + arg.MemberList[i].City +"]</a></dib>";
 65                     }
 66                     $(list).append(tag);
 67                     $(list).removeClass();
 68                 }
 69             }
 70         })
 71     }
 72     <!--获取消息函数-->
 73     function send_msg(self) {
 74         var to_uid = $('#user_id').val();
 75         var msg = $('#msg_content').val();
 76         $.ajax({
 77             url: '/send_msg',
 78             type: 'GET',
 79             dataType: 'json',
 80             data: {'to_uid': to_uid, 'msg': msg},
 81             success: function (arg) {
 82                 console.log(arg);
 83             }
 84         })
 85     }
 86     function get_msg() {
 87         $.ajax({
 88             url: '/get_msg',
 89             type: 'GET',
 90             dataType: 'json',
 91             success: function (arg) {
 92                 if (arg.status){
 93                     var tag = "<div>" + arg.msg.user_id + "</div><div>" + arg.msg.msg_info + "</div>";
 94                     console.log(tag);
 95                     $('#msg_box').append(tag)
 96                 }
 97                 console.log(arg);
 98                 get_msg()
 99             }
100         })
101     }
102 </script>-->
103 </body>
104 </html>
index.html
def index(request):
    # 初始化用户数据

    base_index_url = '{0}/cgi-bin/mmwebwx-bin/webwxinit?pass_ticket={1}&r={2}'

    index_url = base_index_url.format(BASE_URL, TICKET_DICT['pass_ticket'], int(time.time()))

    user_cookies = {}

    user_cookies.update(LOGIN_COOKIE_DICT)

    user_cookies.update(TICKET_COOKIE_DICT)

    response_init = requests.post(url=index_url,
                                  cookies=LOGIN_COOKIE_DICT,
                                  json={
                                      'BaseRequest': {
                                          'DeviceID': "e199625221824018",
                                          'Sid': TICKET_DICT['wxsid'],
                                          'Skey': TICKET_DICT['skey'],
                                          'Uin': TICKET_DICT['wxuin']
                                      }
                                  })

    response_init.encoding = 'utf-8'

    user_init_data = json.loads(response_init.text)

    USER_INFO.update(user_init_data)

    return render(request, 'index.html', {'info': user_init_data})
Views 用户数据初始化函数

这样可以获取近期联系过的好友、群、公众号,还有一些公众号的信息。下一步我们要获取全部的好友。需要发送另一个请求获取。

4、获取好友列表

  我们接着去看登录成功的web微信请求,查找返回全部好友信息的那一条: webwxgetcontact, URL:https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxgetcontact?pass_ticket=****&r=1487313589641&seq=0&skey=****,这个请求是get请求,链接中passticket和skey可以在票据中拿到,r是时间戳,seq是状态。在发送这个请求的时候我们加上登录成功后的cookie和获取票据时的cookie就可以了。然后将请求到的数据渲染到页面上。

 代码:

我们将index.html中的get_list函数恢复。

 1 def get_list(request):
 2     all_user_cookies = {}
 3 
 4     base_get_list_url = '{0}/cgi-bin/mmwebwx-bin/webwxgetcontact?lang=zh_CN&pass_ticket={1}&r={2}&seq=0&skey={3}'
 5 
 6     get_list_url = base_get_list_url.format(BASE_URL, TICKET_DICT['pass_ticket'], int(time.time()), TICKET_DICT['skey'])
 7 
 8     all_user_cookies.update(LOGIN_COOKIE_DICT)
 9 
10     # all_user_cookies.update(TICKET_COOKIE_DICT)
11 
12     response_list = requests.get(get_list_url, cookies=all_user_cookies)
13 
14     #  我们在获取数据的时候使用response_list.text会默认编码,但是一般我们指定使用'utf-8'进行编码
15 
16     response_list.encoding = 'utf-8'
17 
18     list_info = response_list.text
19 
20     return HttpResponse(list_info)
Views 获取好友列表函数

这样可以将我们想看到的数据显示到页面上。接下来应该选择一个好友,然后给他发送消息了。

5、发送微信消息

  我们回到web微信,发送一个消息,然后看Network里有什么变化。我们会看到一个webwxsendmsg开头的请求,URL:https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxsendmsg?pass_ticket=****,同样,URL中的passticket去票据中取。这个请求是post请求,去查看数据。有三部分:第一部分是我们在获取好友列表的时候用过的,可以直接粘过来。第二部分需要我们去找。ClientMsgId和LocalID可以用时间戳,FromUserName、ToUserName、Content都可以在前端传过来,其中FromUserName也可以在之前初始化数据中找到,Type直接写1即可。我们只实现文字类型的传输。第三部分很简单,只有一个状态值。按照格式复制就可以了。

代码:

我们将index.html中的send_list函数恢复。

 1 def send_msg(request):
 2     base_send_url = '{0}/cgi-bin/mmwebwx-bin/webwxsendmsg?pass_ticket={1}'
 3     send_url = base_send_url.format(BASE_URL, TICKET_DICT['pass_ticket'])
 4     from_uid = USER_INFO['User']['UserName']
 5     to_uid = request.GET.get('to_uid')
 6     msg = request.GET.get('msg')
 7 
 8     # current_time = str(int(time.time() * 1000)) + str(random.random())[:5].replace('.', '')
 9 
10     form_data = {
11         'BaseRequest': {
12               'DeviceID': "e199625221824018",
13               'Sid': TICKET_DICT['wxsid'],
14               'Skey': TICKET_DICT['skey'],
15               'Uin': TICKET_DICT['wxuin']
16         },
17         'Msg': {
18             'ClientMsgId': str(time.time()),
19             'Content': '%(content)s',
20             'FromUserName': from_uid,
21             'LocalID': str(time.time()),
22             'ToUserName': to_uid,
23             'Type': 1
24         },
25         'Scene': 0
26     }
27 
28     all_cookies = {}
29 
30     all_cookies.update(LOGIN_COOKIE_DICT)
31 
32     all_cookies.update(TICKET_COOKIE_DICT)
33 
34     form_data_str = json.dumps(form_data)
35 
36     form_data_str = form_data_str % {'content': msg}
37 
38     form_data_bytes = bytes(form_data_str, encoding='utf-8')
39 
40     response_send = requests.post(
41         url=send_url,
42         data=form_data_bytes,
43         cookies=all_cookies,
44         headers={
45             'Content-Type': 'application/json',
46         }
47     )
48 
49     return HttpResponse("ok")
Views 发送消息函数

需要注意的是,在发送消息的时候,我们要先将data进行json.dumps,之后再将发送消息的部分进行bytes转换。否则,汉字会变为ascii编码格式发出。是因为我们在json的时候,会将汉字转换为ascii编码格式,再发送前还会进行一次bytes类型转换。这样就把源数据改变了。也可以在dumps的时候加上一个ensure_ascii=False参数阻止转变成ascii编码格式。这样我们就剩最后一步没有做了。

6、接收微信消息

  接收消息,其实就是服务器将别人发送的消息发送给我们,那么之前说过http是无状态的,说到这里,应该都已经想到了,我们还是要做一个长轮循来监听消息。在web界面登录成功后我们还会看到一个一直在发送的请求,去检查它。没错就是synccheck开头的那个。URL:https://webpush.wx.qq.com/cgi-bin/mmwebwx-bin/synccheck?r=1487320137207&skey=***&sid=****&uin=****&deviceid=****&synckey=****,请求方式:GET,对用get请求方式,URL后面的数据我们也可以通过在requests请求的的时候在参数中添加params传递。在这里r对应的是时间戳,skey、sid、uin都可以在票据中取到。deviceid使用我们之前使用过的就好。synckey稍微有一点麻烦,需要我们构造。其数据可以通过用户初始化数据取到。这个请求发送过去之后,会返回一个值,来告诉浏览器是否有消息发送过来。当收到有消息过来的时候我们就要发送另一个请求:webwxsync开头的那个,URL:https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxsync?sid=****&skey=****&pass_ticket=****,方式是post,URL中的三个参数都可以从票据中获取。post的数据有也都是我们用过的,只有一个"SyncKey",需要到用户初始数据中取,找到那个key就可以拿到。拿到数据之后使用"utf-8"进行编码,之后使用json.loads,将拿到的数据进行分析。首先看数据中的"StatusNotifyCode"是否为0,如果不是,那么数据不做处理,是因为,我们在手机客户端点进一个群的时候就会有数据返回,但是是历史消息,这个我们不要,当有即时消息发送过来的时候刚才的那个key对应的数据为0。然后将数据拿到返回到页面显示即可。

代码:

我们将index.html中的get_msg函数恢复。

def get_msg(request):
    ret = {"status": False, "msg": ''}
    #  构造synckey
    synckey = []
    for i in USER_INFO['SyncKey']['List']:
        synckey.append(str(i['Key']) + '_' + str(i['Val']))
    synckey_str = "|".join(synckey)

    synckey_url = '%s/cgi-bin/mmwebwx-bin/synccheck' % BASE_SYNC_URL

    current_time = str(time.time())

    all_cookies = {}

    all_cookies.update(LOGIN_COOKIE_DICT)

    all_cookies.update(TICKET_COOKIE_DICT)

    respons_synckey = requests.get(
        url=synckey_url,
        cookies=all_cookies,
        params={
            'r': current_time,
            'skey': TICKET_DICT['skey'],
            'sid': TICKET_DICT['wxsid'],
            'uin': TICKET_DICT['wxuin'],
            'deviceid': "e199625221824018",
            'synckey': synckey_str
        }
    )

    content = ""
    if 'selector:"2"' in respons_synckey.text:
        base_get_msg_url = '{0}/cgi-bin/mmwebwx-bin/webwxsync?sid={1}&skey={2}&pass_ticket={3}'
        get_msg_url = base_get_msg_url.format(BASE_URL, TICKET_DICT['wxsid'], TICKET_DICT['skey'], TICKET_DICT['pass_ticket'])
        form_data = {
            'BaseRequest': {
                'DeviceID': "e199625221824018",
                'Sid': TICKET_DICT['wxsid'],
                'Skey': TICKET_DICT['skey'],
                'Uin': TICKET_DICT['wxuin']
            },
            'SyncKey': USER_INFO['SyncKey'],
            'rr': current_time
        }

        respons_get_msg = requests.post(
            url=get_msg_url,
            json=form_data
        )

        respons_get_msg.encoding = 'utf-8'
        res_fetch_msg_dict = json.loads(respons_get_msg.text)
        USER_INFO['SyncKey'] = res_fetch_msg_dict['SyncKey']  # 有消息来到,需要更新SyncKey状态否则会一直是有消息的状态

        print(res_fetch_msg_dict)

        for item in res_fetch_msg_dict['AddMsgList']:
            if item['StatusNotifyCode'] == 0:
                print(item['Content'], ":::::", item['FromUserName'], "---->", item['ToUserName'], )
                ret["status"] = True
                ret['msg'] = {'user_id': item['FromUserName'], 'msg_info': item['Content']}

    return HttpResponse(json.dumps(ret))
Views 获取消息函数

这里需要注意的是,在接收消息后,将用户初始化数据中的"SyncKey"更新为发送的消息中的"SyncKey",如果不更新的话,这条数据就会一直被取到。

 

posted @ 2017-02-17 16:56  善行者无疆  阅读(584)  评论(0编辑  收藏  举报