Web微信模拟

时间:2023-03-10 01:14:47
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里的大部分的参数都是状态值,只有一个'_'我们猜测是时间戳。现在我们就可以试试能不能获取到二维码。

代码:

 <!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div style="width: 300px; margin: 0 auto">
<!--二维码路径-->
<img src="https://login.weixin.qq.com/qrcode/{{ code }}">
</div>
<!--注释掉的部分是稍后请求扫码状态的函数-->
<!--<script src="/static/jquery-3.1.1.js"></script>
<script>
$(function () {
polling();
});
function polling() {
$.ajax({
url: '/long_polling/',
type: 'GET',
dataType: 'json',
success: function (arg) {
if (arg.status == 408){
polling()
}else if (arg.status == 201){
console.log(123);
$('img').attr('src', arg.data);
polling()
}else {
location.href = '/index/'
}
}
})
}
</script>-->
</body>
</html>

login.html

 from django.shortcuts import render

 from django.shortcuts import HttpResponse

 from bs4 import BeautifulSoup

 import re

 import time

 import requests

 import json

 import random

 CURRENT_TIME = None
QCODE = None
LOGIN_COOKIE_DICT = {}
TICKET_COOKIE_DICT = {}
TICKET_DICT = {}
TIPS = 1
BASE_URL = ''
BASE_SYNC_URL = ''
USER_ID = ''
USER_INFO = {}
USER_LIST_DIC = {}
# 这里用不到的全局变量后面会用到 def login(request):
# 登录页面,显示登录的二维码
base_qcode_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&_={0}'
global CURRENT_TIME
CURRENT_TIME = str(time.time())
q_code_url = base_qcode_url.format(CURRENT_TIME)
respons = requests.get(q_code_url)
# 二维码后缀
global QCODE
QCODE = re.findall('uuid = "(.*)";', respons.text)[0] # 拿括号里的内容的列表 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进行保存。之后的请求中需要用到。

代码:

 def long_polling(request):
ret = {'status': 408, 'data': None} try:
global TIPS
base_login_url = 'https://login.wx.qq.com/cgi-bin/mmwebwx-bin/login?loginicon=true&uuid={0}&tip={1}&' \
'r=-940286750&_={2}' login_url = base_login_url.format(QCODE, TIPS, CURRENT_TIME) response_login = requests.get(login_url) if 'window.code=201' in response_login.text:
TIPS = 0
avatar = re.findall("userAvatar = '(.*)';", response_login.text)
ret['status'] = 201
ret['data'] = avatar
elif 'window.code=200' in response_login.text:
ret['status'] = 200
# 扫码点击确认后获取cookie
LOGIN_COOKIE_DICT.update(response_login.cookies.get_dict())
# 获取redirect的url
base_ticket_url = re.findall('redirect_uri="(.*)";', response_login.text)[0]
# 不同的微信号在初始话数据的时候有不同的地址,需要甄别
global BASE_URL
global BASE_SYNC_URL
if base_ticket_url.startswith('https://wx2.qq.com'):
BASE_URL = 'https://wx2.qq.com'
BASE_SYNC_URL = 'https://webpush.wx2.qq.com'
else:
BASE_URL = 'https://wx.qq.com'
BASE_SYNC_URL = 'https://webpush.wx.qq.com'
# 组成获取票据的url
ticket_url = base_ticket_url + '&fun=new&version=v2&lang=zh_CN'
# 获取票据同时获取cookies response_ticket = requests.get(url=ticket_url, cookies=LOGIN_COOKIE_DICT)
TICKET_COOKIE_DICT.update(response_ticket.cookies.get_dict())
# 分析票据
soup = BeautifulSoup(response_ticket.text, 'html.parser')
for tag in soup.find():
TICKET_DICT[tag.name] = tag.string
except Exception as e:
print(e)
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中。

代码:

 <!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div>
<h1>个人信息</h1>
<a style="font-size: 20px; color: #1c5a9c">{{ info.User.NickName }}</a>
<a id="from_user_id">{{ info.User.UserName }}</a>
<p><input id="user_id" type="text" placeholder="请输入用户ID"></p>
<p><input id="msg_content" type="text" placeholder="请输入内容"></p>
<input id="send_msg" onclick="send_msg(this)" type="button" value="发送">
</div>
<div id="msg_box" style="height: 300px; width: 800px; border: solid 1px gray; overflow: auto"> </div>
<h1>最近联系人</h1>
{% for item in info.ContactList %}
<p>
<a style="font-size: 20px; color: #2F72AB">{{ item.NickName }}</a><a>{{ item.UserName }}</a>
<a>{{ item.Signature }}</a>
</p>
{% endfor %}
<div>
<div id="get_list" onclick="get_list()" style="cursor: pointer">获取全部好友</div>
<div class="empty"></div>
</div>
<h1>公众号</h1>
{% for item in info.MPSubscribeMsgList %}
<p>
<a style="font-size: 20px; color: #8a6d3b;">{{ item.NickName }}</a><a style="display: none">{{ item.UserName }}</a>
</p>
<p>
{% for i in item.MPArticleList %}
<div>
<a style="font-size: 18px">{{ i.Title }}</a>
<a href="{{ i.Url }}">{{ i.Digest }}</a>
</div> {% endfor %}
</p>
{% endfor %} <!--注释的部分是在获取好友列表以及发送和接收消息的时候用到的-->
<!--<script src="/static/jquery-3.1.1.js"></script>
<script>
<!--在页面加载好之后启动获取消息的函数-->
$(function () {
get_msg()
});
<!--获取好友列表函数-->
function get_list() {
$.ajax({
url: '/get_list',
type: 'GET',
dataType: 'json',
success: function (arg) {
var list = $("#get_list").siblings()[0];
if ($(list).hasClass('empty')){
var tag = '';
for (var i in arg.MemberList){
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>";
}
$(list).append(tag);
$(list).removeClass();
}
}
})
}
<!--获取消息函数-->
function send_msg(self) {
var to_uid = $('#user_id').val();
var msg = $('#msg_content').val();
$.ajax({
url: '/send_msg',
type: 'GET',
dataType: 'json',
data: {'to_uid': to_uid, 'msg': msg},
success: function (arg) {
console.log(arg);
}
})
}
function get_msg() {
$.ajax({
url: '/get_msg',
type: 'GET',
dataType: 'json',
success: function (arg) {
if (arg.status){
var tag = "<div>" + arg.msg.user_id + "</div><div>" + arg.msg.msg_info + "</div>";
console.log(tag);
$('#msg_box').append(tag)
}
console.log(arg);
get_msg()
}
})
}
</script>-->
</body>
</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函数恢复。

 def get_list(request):
all_user_cookies = {} base_get_list_url = '{0}/cgi-bin/mmwebwx-bin/webwxgetcontact?lang=zh_CN&pass_ticket={1}&r={2}&seq=0&skey={3}' get_list_url = base_get_list_url.format(BASE_URL, TICKET_DICT['pass_ticket'], int(time.time()), TICKET_DICT['skey']) all_user_cookies.update(LOGIN_COOKIE_DICT) # all_user_cookies.update(TICKET_COOKIE_DICT) response_list = requests.get(get_list_url, cookies=all_user_cookies) # 我们在获取数据的时候使用response_list.text会默认编码,但是一般我们指定使用'utf-8'进行编码 response_list.encoding = 'utf-8' list_info = response_list.text 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函数恢复。

 def send_msg(request):
base_send_url = '{0}/cgi-bin/mmwebwx-bin/webwxsendmsg?pass_ticket={1}'
send_url = base_send_url.format(BASE_URL, TICKET_DICT['pass_ticket'])
from_uid = USER_INFO['User']['UserName']
to_uid = request.GET.get('to_uid')
msg = request.GET.get('msg') # current_time = str(int(time.time() * 1000)) + str(random.random())[:5].replace('.', '') form_data = {
'BaseRequest': {
'DeviceID': "e199625221824018",
'Sid': TICKET_DICT['wxsid'],
'Skey': TICKET_DICT['skey'],
'Uin': TICKET_DICT['wxuin']
},
'Msg': {
'ClientMsgId': str(time.time()),
'Content': '%(content)s',
'FromUserName': from_uid,
'LocalID': str(time.time()),
'ToUserName': to_uid,
'Type': 1
},
'Scene': 0
} all_cookies = {} all_cookies.update(LOGIN_COOKIE_DICT) all_cookies.update(TICKET_COOKIE_DICT) form_data_str = json.dumps(form_data) form_data_str = form_data_str % {'content': msg} form_data_bytes = bytes(form_data_str, encoding='utf-8') response_send = requests.post(
url=send_url,
data=form_data_bytes,
cookies=all_cookies,
headers={
'Content-Type': 'application/json',
}
) 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",如果不更新的话,这条数据就会一直被取到。