WUT自动化健康打卡

众所周知,WUT是一所为学生健康着想的大学,需要学生每日在晚8点前进行健康打卡,但是heavytiger同学经常摸鱼会忘记打卡,引发年级群中的“友好通知”,这样会让人很困扰,恰逢heavytiger同学某日逛B站时,发现一研究生学长通过python实现了自动化打卡,特研究一日,实现了自动化健康打卡。

故事的开始

闲逛B站时发现一个实现自动化健康打卡的视频,恰好本人亦有困扰,于是点进去研究了以下,发现恰好是我校研究生学长(世界真小,视频地址如下,请一键三连:Python 每天自动打卡小程序_哔哩哔哩_bilibili

该学长使用了pyautogui库模拟用户通过鼠标点击,登录微信,点击小程序进行签到,但是我的微信死活不支持一键登录,必须使用手机扫码,再加上本科生没有常年开机的电脑,通过bat实现有些不太现实,于是另辟蹊径,考虑使用python的requests库实现相同功能。

分析健康打卡原理

使用手机进行抓包操作也不太方便,于是考虑使用电脑进行抓包,在windows笔电上登录微信后进入打卡小程序,使用Fiddler进行抓包,伪造证书后,实现了中间人攻击,获取到了https包并成功解包。

image-20211209222108891

小程序和后台使用json进行数据传输,text使用base64编码,很好解码

除了以上的几个Servlet之外,通过在github上搜轮子(轮子地址:xiaozhangtongx/WHUT-JKRBTB: 武汉理工大学健康打卡小脚本 (github.com)),了解到本校的健康打卡小程序只能在微信端解绑定后才能再次打卡。

具体流程如下:

  1. 请求checkBind接口实现登录操作,登陆成功后,返回会话id即JSESSIONID,之后的连接均通过该SessionId进行创建。

  2. 登录成功后,调用bindUserInfo接口查看绑定用户的信息,若此处没有在用户微信手机上解绑,后台应该持有一个用户微信的token导致mybatis查询不为空,导致后期的打卡monitorRegister接口失败

    也可以在fiddler中找到自己的code值,模拟手机微信登录打卡,可以但没必要,因为门槛高。

    (请忽视报错,不想修改截图了,啊吧啊吧)

    image-20211209224013336

  3. 调用monitorRegister接口,通过该接口上传数据,内容大致如下(PS: 是否出省份的key是isLeaveChengdu 不愧是你啊WUT,也就你找这种外包了):

    image-20211209231909197

  4. 调用cancelBind接口,通过该接口传入SessionId取消绑定,否则同理,微信估计也登不上去了。

代码简介

完整目录结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
E:\COLLEGEDATA\打卡
│ sign.py

├─jsons
│ user.json

├─logs
│ days.log
│ error.log
│ run.log

└─test
api.login.checkBind.json
monitorRegister.json
signin.json
testJson.py
testlog.log
打卡.saz

其中重要部分包括:

打卡脚本sign.py

jsons/user.json用于存储用户信息(帮着把室友的也加进去了);

logs/*.log用来存储运行记录,days.log在linux下重定向保存运行输出与报错信息,error.log用来存储可预知的报错,即按程序中的设定输出异常信息,run.log用来存储运行时的正常日志。

具体的代码不在过多介绍,就是简单的requests库调用

可能出现的错误:

可能出现使用requests请求时出现异常失败的情况,原因是,可能正在科学上网,使用了某端口进行转接作为代理服务器,此时需要手动设置:

win -> 键入Internet属性 -> 点击连接 -> 点击局域网设置 -> 禁用代理服务器,选择自动检测设置

image-20211210000041198

配置成如下所示,即可使用requests库发送请求:

image-20211210000129630

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
#!/usr/bin/env python3
# -*- coding: utf8 -*-
import json
import requests
import random
import time

# 随机User-Agent
useragentlist = [
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.143 Safari/537.36 MicroMessenger/7.0.9.501 NetType/WIFI MiniProgramEnv/Windows WindowsWechat",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/22.0.1207.1 Safari/537.1",
"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/531.21.8 (KHTML, like Gecko) Version/4.0.4 Safari/531.21.10",
"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; en-US; rv:1.9.2.14) Gecko/20110218 AlexaToolbar/alxf-2.0 Firefox/3.6.14"
]

# 随机温度
temperature = ["36\"C~36.5°C", "36.5°C~36.9°C"]

class User:
def __init__(self, sn, idCard):
self.sn = sn
self.idCard = idCard

def parseJSON(dct):
if isinstance(dct, dict):
user = User(str(dct["sn"]), str(dct["idCard"]))
return user
return dct

def readJson():
fileName = 'jsons/user.json'
with open(fileName, 'r', encoding='utf-8') as f:
userList = json.load(f)
if isinstance(userList, dict):
users = userList["users"]
for i in range(len(users)):
users[i] = User.parseJSON(users[i])
return users

def writeError(logs):
fileName = 'logs/error.log'
with open(fileName, 'a', encoding='utf-8') as f:
f.write(time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()) + ': ')
f.write(logs)

def writeLog(logs):
fileName = 'logs/run.log'
with open(fileName, 'a', encoding='utf-8') as f:
f.write(time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()) + ': ')
f.write(logs)

def request_sessionId(json_data):
url = "https://zhxg.whut.edu.cn/yqtjwx/api/login/checkBind"
headers = {
"Accept-Encoding": "gzip, deflate, br",
"content-type": "application/json",
"Referer": "https://servicewechat.com/wxa0738e54aae84423/17/page-frame.html",
"X-Tag": "flyio",
"Accept-Language": "zh-cn",
"Connection": "keep - alive",
"Host": "zhxg.whut.edu.cn"
}
headers['User-Agent'] = random.choice(useragentlist)
r = requests.post(url=url, headers=headers, json=json_data)
result = json.loads(r.text)
sessionId = result['data']['sessionId']

if result["status"] != True :
# 说明登录失败
writeError("StudentId: " + str(json_data["sn"]) + " SessionId: " + str(sessionId) + " Error: 登录失败\n")
writeError(str(result) + "\n")
else :
writeLog("StudentId: " + str(json_data["sn"]) + " SessionId: " + str(sessionId) + " Data: 登录成功\n")
return str(sessionId)

def request_bindUserInfo(sessionId, json_data):
url = "https://zhxg.whut.edu.cn/yqtjwx/api/login/bindUserInfo"
headers = {
"Accept-Encoding": "gzip, deflate, br",
"content-type": "application/json",
"Referer": "https://servicewechat.com/wxa0738e54aae84423/17/page-frame.html",
"Cookie": "JSESSIONID=%s" % (sessionId),
"Accept": "*/*",
"X-Tag": "flyio",
"Accept-Language": "zh-cn",
"Connection": "keep - alive",
"Host": "zhxg.whut.edu.cn"
}
headers['User-Agent'] = random.choice(useragentlist)
r = requests.post(url=url, headers=headers, json=json_data)
result = json.loads(r.text)
if result["status"] != True :
# 说明此时已在其他地方登录
writeError("StudentId: " + str(json_data["sn"]) + " SessionId: " + str(sessionId) + " Error: 已被绑定\n")
writeError(str(result) + "\n")
else :
writeLog("StudentId: " + str(json_data["sn"]) + " SessionId: " + str(sessionId) + " Data: 未被绑定\n")

def request_monitorRegister(sessionId, user_data, province, city, county, street):
currentAddress = str(province) + str(city) + str(county) + str(street)
url = "https://zhxg.whut.edu.cn/yqtjwx/./monitorRegister"
headers = {
"Accept-Encoding": "gzip, deflate, br",
"content-type": "application/json",
"Referer": "https://servicewechat.com/wxa0738e54aae84423/17/page-frame.html",
"Cookie": "JSESSIONID=%s" % (sessionId),
"Accept": "*/*",
"X-Tag": "flyio",
"Accept-Language": "zh-cn",
"Connection": "keep - alive",
"Host": "zhxg.whut.edu.cn"
}
headers['User-Agent'] = random.choice(useragentlist)
json_data = {
"diagnosisName": "",
"relationWithOwn": "",
"currentAddress": currentAddress,
"remark": "",
"healthInfo": "正常",
"isDiagnosis": 0,
"isFever": 0,
"isInSchool": "1",
"isLeaveChengdu": 0,
"isSymptom": "0",
"temperature": random.choice(temperature),
"province": province,
"city": city,
"county": county
}
r = requests.post(url=url, headers=headers, json=json_data)
result = json.loads(r.text)
if result["status"] != True :
# 说明每日打卡失败
writeError("StudentId: " + str(user_data["sn"]) + " SessionId: " + str(sessionId) + " Error: 打卡失败\n")
writeError(str(result) + "\n")
return False
else :
writeLog("StudentId: " + str(user_data["sn"]) + " SessionId: " + str(sessionId) + " Data: 打卡成功\n")
return True


def cancelBind(sessionId, json_data):
url = "https://zhxg.whut.edu.cn/yqtjwx/api/login/cancelBind"
headers = {
"Accept-Encoding": "gzip, deflate, br",
"content-type": "application/json",
"Referer": "https://servicewechat.com/wxa0738e54aae84423/17/page-frame.html",
"Cookie": "JSESSIONID=%s" % (sessionId),
"Connection": "keep - alive",
"Host": "zhxg.whut.edu.cn"
}
headers['User-Agent'] = random.choice(useragentlist)
r = requests.post(url=url, headers=headers)
result = json.loads(r.text)
if result["status"] != True :
# 说明解绑失败
writeError("StudentId: " + str(json_data["sn"]) + " SessionId: " + str(sessionId) + " Error: 解绑失败\n")
writeError(str(result) + "\n")
else :
writeLog("StudentId: " + str(json_data["sn"]) + " SessionId: " + str(sessionId) + " Data: 解绑成功\n")

if __name__ == '__main__':
users = readJson()
FLAG = True
for u in users:
data = {'sn': u.sn, 'idCard': u.idCard}
id = request_sessionId(data)
request_bindUserInfo(id, data)
res = request_monitorRegister(id, data, "湖北省", "武汉市", "洪山区", "工大路")
if res == False:
# 说明打卡失败
print(time.strftime('%Y-%m-%d', time.localtime()) + ": 学号为: " + str(u.sn) + "的用户出现错误!")
FLAG = False
cancelBind(id, data)
if FLAG == False:
print(time.strftime('%Y-%m-%d', time.localtime()) + ": 每日填报完成,但存在错误!")
else:
print(time.strftime('%Y-%m-%d', time.localtime()) + ": 每日填报完成!")

部署到服务器

服务器是之前几十块买的阿里云的服务器,运行CentOS 7操作系统

使用Xshell,Xftp等工具将文件传入到服务器中,测试完毕后,使用crontab设置为定时任务

使用命令crontab -e,将使用vi打开定时任务清单,自行配置即可,使用如下语句,设定每天中午12:45分执行命令,将报错以及运行结果输出到./logs/days.log中:

1
2
[root@CentOS ~]# crontab -l
45 12 * * * cd /usr/local/src/daySign && ./sign.py >> ./logs/days.log 2>&1

多次打卡会提示打卡失败,可在error.log中查看:

image-20211210000948325

打卡时的响应数据会存储在run.log中,可以查看:

image-20211210001054621

学校的服务器凌晨运维,凌晨手动测试为啥提交不上的时候不小心把log删除了,因此上面没有打卡成功数据,成功时会在run.log增加一条记录打卡成功

若有用户在手机上登录未解绑,error.log中会报错,otherData中报MyBatis调Mapper中查询selectOne错误,出现问题时,自然懂得都懂。

参考资料

[1] crontab命令(Linux定时任务) - Linux命令大全教程™ (yiibai.com)

[2] Python 每天自动打卡小程序_哔哩哔哩_bilibili

[3] xiaozhangtongx/WHUT-JKRBTB: 武汉理工大学健康打卡小脚本 (github.com)


-------------本文到此结束 感谢您的阅读-------------
谢谢你请我喝肥宅快乐水(๑>ڡ<) ☆☆☆