首先回顾下网页微信登陆的一般流程

  1、打开浏览器输入网址

  2、使用手机微信扫码登陆

  3、进入用户界面

1、打开浏览器输入网址

首先打开浏览器输入web微信网址,并进行监控:

https://wx.qq.com/

可以发现网页中包含了一个新的url,而这个url就是二维码的来源。

https://login.weixin.qq.com/qrcode/wbfd1Z-a0g==

可以猜测一下获取url的一般网址就是https://login.weixin.qq.com/qrcode,而wbfd1Z-a0g==肯定就是通过请求前面的url后传入的参数,最终生成二维码图片。

此时再监控此次请求的network,去寻找,究竟是什么时候返回给客户端wbfd1Z-a0g==字符串的。

最终在网址: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&_=1532592134738的response里面找到了二维码参数。

这个网址里面_=1532592134738里面传入的看着像一个时间戳。此时我们先新建一个Django项目,在login页面向上面的网址发送请求。取得二维码参数,再传给前台的img标签,就可以做出二维码登陆页面。

新建一个Django项目、并新建一个app wechat:

将主url路由指向wechat app里面的url, 并写好一个login url,让用户从我们这边开始登陆微信。

wechat urls:

views内的login函数:

from django.shortcuts import render, HttpResponse, redirect
import requests, time
import re, json
from bs4 import BeautifulSoup
# Create your views here. def login(req):
ctime = time.time() * 1000 # 模拟一个相同的时间戳
base_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}'
url = base_url.format(ctime) # 字符串拼接,生成新的url
response = requests.get(url) # 向新的url发送get请求
xcode_list = re.findall('window.QRLogin.uuid = "(.*)";', response.text) # 通过正则表达式,提取到对于的参数列表['YZzTdz9m_A==', 'YZzTdz9m_A==']
req.session['xcode'] = xcode_list[0] # 获取到参数,存入session内
return render(req, 'wechat/login.html', {'xcode': xcode_list[0]}) # 返回给login页面此参数

在templates目录里面创建一个wechat目录,在wechat目录创建一个login.html页面,传入参数。

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<img id="xcode" style="width: 200px; height: 200px; margin:100px 40%" src="https://login.weixin.qq.com/qrcode/{{ xcode }}">
</body>
</html>

因为加入了session功能需要先run一下

python3 manage.py makemigrations
python3 mange.py migrate

运行并访问login:

此时,就完成第一个二维码页面了。

第二步,有了二维码,手机扫描是如何进行交互的?是不是这个网页有个后台一直在请求某个网页,一旦扫码验证就进行下一步操作?继续监控请求

思路渐渐清晰了,在请求登陆的时候大概是如下的流程:

打开一个失效的url:

再查看扫描完的URL的返回:

最后确认登陆:

再次观察,会发现这个url其实是一个长轮询,一直在请求认证结果。pending的是在等待的当前请求。而在我们的项目内部,因为浏览器存在一个同源策略,所以无法完成在前端直接获取结果。此时改变一下思路:可以通过login内部写个url 偷偷的访问后端的视图,视图层再通过requests模块去发送请求获取结果返回给前端。

另外关于url的三种返回:

1、408代表没有人扫码(需要重新发送)
2、201代表已经有用户扫码,后面的参数返回的是用户头像
3、200代表用户已经登陆

定义一下检查的url:

    path('check_login/', views.check_login),

login.html页面加入javascript

<script src="/static/jquery.min.js"></script>
<script>
tip = 1
function checkLogin() {
$.ajax({
url:'/wechat/check_login/',
data: {'tip': tip},
type: 'GET',
dataType: 'JSON',
success:function (arg) {
if (arg.code == 201){
// 有人扫码了
$('#xcode').attr('src', arg.data);
checkLogin();
tip = 0;
}else if (arg.code == 408){
checkLogin();
}else if (arg.code == 200){
window.location.href='/wechat/index/'
}
}
})
}
checkLogin();
</script>

这边需要提醒的是当用户扫码之后的url发送的参数中有一个tip参数有变动,从1变成0.

def check_login(req):
tip = req.GET.get('tip') # 标记是否扫码
# 自定义返回的json数据格式
ret = {
'code': 408, # 初始值408代表没有任何操作
'data': None
}
ctime = time.time() * 1000 # 根据得到的url,伪造匹配的时间戳
base_url = 'https://login.wx.qq.com/cgi-bin/mmwebwx-bin/login?loginicon=true&uuid={0}&tip={1}&r=903313058&_={2}'
url1 = base_url.format(req.session['xcode'], tip, ctime) # 字符串修饰
r1 = requests.get(url1) # 获取响应
if 'window.code=201' in r1.text:
# 有人扫码
v = re.findall("window.userAvatar = '(.*)';", r1.text)
avatar = v[0]
ret['code'] = 201 # 状态码,代表有人扫码了
ret['data'] = avatar # 用户头像
elif 'window.code=200;' in r1.text:
# 扫码之后,点击确定登录
req.session['login_cookie'] = r1.cookies.get_dict() # 获取确认登陆的cookie
uri = re.findall('window.redirect_uri="(.*)";', r1.text) # 之前的图片,已经发现这里是一个重定向路由,所以获取重定向的路由
# https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxnewloginpage?ticket=AfpJ_ZmEJGcJ8iU62SiPyuTo@qrticket_0&uuid=gZHgYImvDQ==&lang=zh_CN&scan=1532410951
redirect_url = '{0}&fun=new&version=v2'.format(uri[0]) # 字符串拼接,形成新的url
# 获取凭证
r2 = requests.get(redirect_url) # 网页重定向的时候,会返回凭证用来进行之后的验证
ticket_dict = {}
soup = BeautifulSoup(r2.text, 'html.parser') # 标签文本实例化bs对象
for item in soup.find(name='error').children: # 找到需要的凭证
ticket_dict[item.name] = item.text # 凭证存入字典
req.session['ticket_dict'] = ticket_dict # 凭证存入session
req.session['ticket_cookie'] = r2.cookies.get_dict() # session中存入获取重定向的cookie
ret['code'] = 200 # 200表示确定登陆了
req.session['is_login'] = True # 给后面的url判断是否登陆
return HttpResponse(json.dumps(ret)) # Json序列化返回

最后创建一个新的路由:

    path('index/', views.index),

创建一个新的html和视图函数index:

def index(req):
return render(req, 'wechat/index.html')

此时,便可以完成web微信登陆功能。

留一份确认登陆的凭证:

<error>
<ret>0</ret>
<message></message>
<skey>@crypt_41bad7c6_8bff0cc40ih18fba431b5a7e87l63bc7</skey>
<wxsid>LkgBf9UVGIkHdg80</wxsid>
<wxuin>15749213440</wxuin>
<pass_ticket>QvkUzyBHDWQ8HJDowkqw6rmqQ48weyrwDiNPTbn%2FnZl7tk%3D</pass_ticket>
<isgrayscale>1</isgrayscale>
</error>

获取登陆信息

根据平时登陆web wechat的常识,登陆确认之后肯定是跳往目的路由,初始化一些用户信息,获取用户的各种资料等等。这只是猜测怎么办?打开web 微信继续检测network。这一次从确认登陆完成进行截图分析。

url: https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxinit?r=696489291&lang=zh_CN&pass_ticket=QvkUzyBiMqnrFcw6rmqQ4I7E2DiNxTUDPTbn%252FnZl7tk%253D

url里面有pass_ticket参数.

看到凭证组成的form_data,另外此次是POST请求。由于response数据太多了,在index里面我们先写一个for循环看看都有哪些key值,看看能不能根据key值的命名规范,找到规律。

视图函数index:

def index(req):
# 判断是否已经登陆
if not req.session.get('is_login'):
return redirect('/wechat/login/') # 发送post请求,根据ticket_dict进行构造数据
# https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxinit?r=892259194&pass_ticket=TS7TEfumVaVzKhn%252FrnLKS2zZyhixJDEYxlXqGgQVplQ%253D
base_url = 'https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxinit?r=892259194&pass_ticket={0}'
url = base_url.format(req.session['ticket_dict']['pass_ticket'])
# BaseRequest
# :
# {Uin: "184513440", Sid: "U4WojQwDRwKqdeMs", Skey: "", DeviceID: "e409571391728320"} # 伪造的数据格式样板 form_data = { # 伪造数据
'BaseRequest': {
'DeviceID': "e409571391728320",
'Sid': req.session['ticket_dict']['wxsid'],
'Skey': req.session['ticket_dict']['skey'],
'Uin': req.session['ticket_dict']['wxuin']
}
}
r1 = requests.post(
url=url,
json=form_data
)
r1.encoding = r1.apparent_encoding # 使用默认编码原则
user_info = json.loads(r1.content)
for key in user_info:
print(key)
return render(req, 'wechat/index.html', {"user_info": user_info})

结果:

BaseResponse
Count
ContactList
SyncKey
User
ChatSet
SKey
ClientVersion
SystemTime
GrayScale
InviteStartCount
MPSubscribeMsgCount
MPSubscribeMsgList
ClickReportInterval

可以猜测ContactList应该是最近联系人;User应该是用户本身,MPSubscribeMsgList应该是订阅号信息。

再打印一下User里面的key看一下,视图函数改一句就好了:

    for key in user_info['User']:
print(key)

结果key:

Uin
UserName
NickName
HeadImgUrl
RemarkName
PYInitial
PYQuanPin
RemarkPYInitial
RemarkPYQuanPin
HideInputBarFlag
StarFriend
Sex
Signature
AppAccountFlag
VerifyFlag
ContactFlag
WebWxPluginSwitch
HeadImgFlag
SnsFlag

果然,现在这么看应该是登陆用户的信息了。

同理测试一下订阅号:

{
'UserName': '@8fbbad518f4d3cf22ebd0a1b0b6d2cb5',
'MPArticleCount': 4,
'MPArticleList': [
{
'Title': '80%的爱情还没开始就会终结。关于错过的遗憾,20 年前几米就教给你了',
'Digest': '你活得不开心,因为你太把自己当大人。',
'Cover': 'http://mmbiz.qpic.cn/mmbiz_jpg/ib0l8DHhOSLuh527WuVHWnnIXwxR5s37pgacZriaKQPUkKEqArKEa9v6tH8buhrNHZB3IMMgf33RCKxtziazCTPrQ/640?wxtype=jpeg&wxfrom=0',
'Url': 'http://mp.weixin.qq.com/s?__biz=MzIzMzk1MzE0NQ==&mid=2247485983&idx=1&sn=6e268e95cdaf04c21114b53f1285bba4&chksm=e8fc8809df8b011fba6a8092400b312102e247780168d56f69c6863e31e7c4f3945253b1f259&scene=0#rd'
},
{...}
],
'Time': 1532608404,
'NickName': '新世相读书会'
}

此时我们对index进行处理:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>个人信息:{{ user_info.User.NickName }}</h1>
<ul>
{% for user in user_info.ContactList %}
<li>{{ user.NickName }}</li>
{% endfor %}
</ul>
<a href="/wechat/contact_all/">查看更多联系人</a>
<h3>公众号信息</h3>
{% for msg in user_info.MPSubscribeMsgList %}
<div>
<h3>{{ msg.NickName }}</h3>
<ul>
{% for article in msg.MPArticleList %}
<li><a href="{{ article.Url }}">{{ article.Title }}</a></li>
{% endfor %}
</ul>
</div>
{% endfor %}
</body>
</html>

中间建立了一个新的url,用来显示所有联系人。先建立一个路由跟视图函数,其他暂时不管。

稍微修改了一下视图函数,加了个User字典到session里面,后面要用到:

def index(req):
# 判断是否已经登陆
if not req.session.get('is_login'):
return redirect('/wechat/login/') # 获取最新联系人、并展示
# 发送post请求,根据ticket_dict进行构造数据
# https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxinit?r=892259194&pass_ticket=TS7TEfumVaVzKhn%252FrnLKS2zZyhixJDEYxlXqGgQVplQ%253D
base_url = 'https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxinit?r=892259194&pass_ticket={0}'
url = base_url.format(req.session['ticket_dict']['pass_ticket'])
# BaseRequest
# :
# {Uin: "184513440", Sid: "U4WojQwDRwKqdeMs", Skey: "", DeviceID: "e409571391728320"} form_data = {
'BaseRequest': {
'DeviceID': "e409571391728320",
'Sid': req.session['ticket_dict']['wxsid'],
'Skey': req.session['ticket_dict']['skey'],
'Uin': req.session['ticket_dict']['wxuin']
}
}
r1 = requests.post(
url=url,
json=form_data
)
r1.encoding = r1.apparent_encoding
user_info = json.loads(r1.content)
req.session['current_user_info'] = user_info['User']
# for k, v in user_info.items():
# print(k, v)
# for user in user_info['ContactList']:
# print(user['NickName'])
# for msg in user_info['MPSubscribeMsgList']:
# print(msg['NickName'])
return render(req, 'wechat/index.html', {"user_info": user_info})

结果示意图:

此时解决查看联系人的问题,还是老方法,继续监控network。这里就不再卖关子了。getcontact这个就是获取所有联系人的url。

不必多说,继续伪造:

    path('contact_all/', views.contact_all),

视图函数:

def contact_all(req):
# https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxgetcontact?pass_ticket=z%252BTVxEioLl3A5arKy%252BUMHbeTME%252BmAkJEulNYIYgGcpw%253D&r=1532421128264&seq=0&skey=@crypt_41bed7c6_e213390395ab3a4ef54bfef5d004f719
base_url = 'https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxgetcontact?pass_ticket={0}&r={1}&seq=0&skey={2}'
url = base_url.format(
req.session['ticket_dict']['pass_ticket'],
time.time() * 1000,
req.session['ticket_dict']['skey'],
) # url拼接
all_cookies = {}
all_cookies.update(req.session['login_cookie'])
all_cookies.update(req.session['ticket_cookie']) # 带入所有的cookies r1 = requests.get(url, cookies=all_cookies)
r1.encoding = r1.apparent_encoding
contact_dict = json.loads(r1.content)
for item in contact_dict:
print(item)
# for item in contact_dict['MemberList']:
# # if item['RemarkName'] == '宇宙第一帅':
# # print(item)
return render(req, 'wechat/contact_all.html', {'contact_dict': contact_dict})

index.html

打印了所有的最外层的key:

BaseResponse
MemberCount
MemberList
Seq

上面被注释的那段代码,可以用来查找你备注的某个人的信息。监控里面看到的username其实是用户在微信里面的唯一ID。

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<div>
<h1>联系人列表</h1>
<ul>
{% for item in contact_dict.MemberList %}
<li>{{ item.UserName|safe }} --- {{ item.NickName|safe }}</li>
{% endfor %}
</ul>
</div>
</body>
</html>

截图:

好了,现在到最紧张的最后一步了,如何发送消息?

现在只需要伪造信息就好了,这里我们可以看见就是根据username的唯一id来发送信息的。

在contact网页里面添加如下代码:

<div>
<h1>发送消息</h1>
<p>接受者:<input type="text" id="recv" /></p>
<p>内容:<input type="text" id="content" /></p>
<input type="button" id="btn" value="Send" />
</div> <script src="/static/jquery.min.js"></script>
<script>
$(function() {
$('#btn').click(function() {
var recv = $('#recv').val();
var content = $('#content').val();
$.ajax({
url: '/wechat/send_msg/',
type: 'GET',
data: {'recv': recv, 'content': content},
success: function(arg) {
console.log(arg)
}
})
})
})
</script>

路由:

    path('send_msg/', views.send_msg),

视图函数:

def send_msg(req):
recv = req.GET.get('recv')
content = req.GET.get('content')
# https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxsendmsg?pass_ticket=wtwzy%252F7fxQgJaTA511weqPXIkIGSJmZdCRATgZdIfYY%253D
base_url = 'https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxsendmsg?pass_ticket={0}'
url = base_url.format(req.session['ticket_dict']['pass_ticket'])
ctime = time.time() * 1000
form_data = { # 伪造数据格式
'BaseRequest': {
'DeviceID': "e939509344931677",
'Sid': req.session['ticket_dict']['wxsid'],
'Skey': req.session['ticket_dict']['skey'],
'Uin': req.session['ticket_dict']['wxuin']
},
'Msg': {
'ClientMsgId': ctime,
'Content': content,
'FromUserName': req.session['current_user_info']['UserName'],
'LocalID': ctime,
'ToUserName': recv,
'Type': 1, # 文本
},
'Scene': 0
}
all_cookies = {}
all_cookies.update(req.session['login_cookie'])
all_cookies.update(req.session['ticket_cookie']) # 带入cookie,试验过证明是需要的
r1 = requests.post(
url=url,
data=bytes(json.dumps(form_data, ensure_ascii=False), encoding='utf-8'),
cookies=all_cookies,
headers={
'Content-Type': 'application/json' # 这句话用来表示,需要序列化成json数据;也可以去掉data跟headers直接用json=form_data来实现
}
)
print(r1.text)
return HttpResponse('.....')

测试一下:

em。。。。。。

对,然后被拉黑了。至于接收消息,发送图片什么的其实都可以自己通过网络监控做到的。这里就不再多提了。

收工!

Python 爬虫五 进阶案例-web微信登陆与消息发送的更多相关文章

  1. C#开发微信门户及应用(19)-微信企业号的消息发送(文本、图片、文件、语音、视频、图文消息等)

    我们知道,企业号主要是面向企业需求而生的,因此内部消息的交流显得非常重要,而且发送.回复消息数量应该很可观,对于大企业尤其如此,因此可以结合企业号实现内部消息的交流.企业号具有关注安全.消息无限制等特 ...

  2. 基于Flask 实现Web微信登陆

    网页版微信登陆网址 https://login.wx.qq.com/ 获取微信登陆的二维码 在浏览器中访问登陆接口 https://login.wx.qq.com/ 我们查找二维码的图片可以看到 其中 ...

  3. Python 爬虫四 基础案例-自动登陆github

    GET&POST请求一般格式 爬取Github数据 GET&POST请求一般格式 很久之前在讲web框架的时候,曾经提到过一句话,在网络编程中“万物皆socket”.任何的网络通信归根 ...

  4. python爬虫(五)_urllib2:Get请求和Post请求

    本篇将介绍urllib2的Get和Post方法,更多内容请参考:python学习指南 urllib2默认只支持HTTP/HTTPS的GET和POST方法 urllib.urlencode() urll ...

  5. Python爬虫(十一)_案例:使用正则表达式的爬虫

    本章将结合先前所学的爬虫和正则表达式知识,做一个简单的爬虫案例,更多内容请参考:Python学习指南 现在拥有了正则表达式这把神兵利器,我们就可以进行对爬取到的全部网页源代码进行筛选了. 下面我们一起 ...

  6. Python爬虫(十三)_案例:使用XPath的爬虫

    本篇是使用XPath的案例,更多内容请参考:Python学习指南 案例:使用XPath的爬虫 现在我们用XPath来做一个简单的爬虫,我们尝试爬取某个贴吧里的所有帖子且将该帖子里每个楼层发布的图片下载 ...

  7. Python 爬虫 (五)

    # 头条街拍图片爬取 1 import re import requests from urllib import request import json import os i = 0 header ...

  8. Python爬虫 —— 知乎之selenium模拟登陆获取cookies+requests.Session()访问+session序列化

    代码如下: # coding:utf-8 from selenium import webdriver import requests import sys import time from lxml ...

  9. Python实现WEB QQ 登录与消息发送(第一版本 2015.06.26)

    WEB QQ的登录步骤与协议,需要的度娘下,很多. 转载说明来源:http://www.cnblogs.com/ryhan/p/4602762.html 我这实现是参考了度娘搜的 和自己抓包分析的. ...

随机推荐

  1. Lua 学习笔记(五)函数

    函数的定义:在Lua中,函数是一种对语句和表达式进行抽象的主要机制. 一.函数基本用法        在Lua中,      1.函数既可以完成某项特定的任务.(被视为一条语句)      2.也可以 ...

  2. EF框架之三种模式

    使用EF之前必须要对EF有个宏观的了解.学习任何一种技术都要像门卫一样问几个问题. 第一,它是谁? 第二,从哪里来? 第三,到哪里去? 默念一遍:不谋全局者,不足谋一域. Entity Framewo ...

  3. 利用Code128字体将文本转换为code128条形码

    利用Code128字体将文本转换为code128条形码[转]   最近在做仓储的项目,许多的打印文件都包含条形码,之前一直使用C39P24DhTt字体直接转换为39码,但是最近要求使用code128编 ...

  4. 解决Qt程序在Linux下无法输入中文的办法

    解决Qt程序在Linux下无法输入中文的办法 一位网友问我怎样在Linux的Qt的应用程序中输入中文,我一開始认为不是什么问题,可是后面自己尝试了一下还真不行.不仅是Qt制作的应用程序,就连Qt Cr ...

  5. hdu4521 小明系列的问题——小明序列(LIS变种 (段树+单点更新解决方案))

    链接: huangjing 题目:中文题目 思路: 1:这个题目假设去掉那个距离大于d的条件,那么必定是一个普通的LIS.可是加上那个条件后就变得复杂了.我用的线段树的解法. . .就是採用延迟更新的 ...

  6. 2017TSC世界大脑与科技峰会,多角度深入探讨关于大脑意识

    TSC·世界大脑与科技峰会是全世界范围内的集会,多角度深入探讨关于大脑意识体验来源这一根本话题,我们是谁,现实的本质是什么,我们所处宇宙空间的本质为何.该峰会由亚利桑那大学意识研究中心主办. 会议时间 ...

  7. [刷题]算法竞赛入门经典(第2版) 6-7/UVa804 - Petri Net Simulation

    题意:模拟Petri网的执行.虽然没听说过Petri网,但是题目描述的很清晰. 代码:(Accepted,0.210s) //UVa804 - Petri Net Simulation //Accep ...

  8. spring是什么???

    1.是一个容器 2.用于降低代码间的耦合度3.根据不同的代码采用ioc和aop两种技术解耦合...

  9. CentOS安装并设置MariaDB

    作者: 铁锚 日期: 2013年12月27日 部分参考: Centos 使用YUM安装MariaDB 说明: 首先必须能链接外网. 如果不能直接访问,那也可以设置代理,请参考: 在内网机器上设置yum ...

  10. #WEB安全基础 : HTML/CSS | 0x5a标签拓展和class、id属性的使用

    a标签不只是能链接到其他网页,也能链接到网页中的元素 class属性让你用CSS对特定的元素进行修饰 这些是一个网页设计者的有力武器 这节课还是一个index.html文件   以下是代码 <h ...