zoukankan      html  css  js  c++  java
  • 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=" 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;  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",如果不更新的话,这条数据就会一直被取到。

  • 相关阅读:
    TOMCAT热部署 catalina.home catalina.base
    spring boot test MockBean
    源码分析ConcurrentHashMap
    源码分析Thread
    源码分析String
    jvm 占用高的问题定位
    docker 资源限制
    数据库设计方案与优化
    linux搜索查找类命令|--grep指令
    linux搜索查找类命令|--locate命令
  • 原文地址:https://www.cnblogs.com/sxzwj/p/6409016.html
Copyright © 2011-2022 走看看