注1:本程序只对长沙理工大学做了适配,其它大学需要自行抓包改代码哦~

注2:对于首次打卡十几天后易班对校本化的授权可能会失效,这次重新加上判断授权失败重新授权的代码。已于2020-04-11测试无误。

😊视频讲解

😎前沿

项目地址:https://github.com/looyeagee/yiban_auto_submit

长沙理工大学自助打卡地址:https://yiban.looyeagee.cn/

学校需要每天打卡,而健康的我打卡内容每天也是一样的。所以我就想实现一个自动打卡的程序。(刚刚得到一个坏消息,还要每天截图,我很懒,所以写又了个自动截图)

🚀准备工具

HttpCanary - 安卓手机抓包大师

HttpCanary是一款功能强大的HTTP/HTTPS/HTTP2网络包抓取和分析工具,你可以把他看成是移动端的Fiddler或者Charles,但是HttpCanary使用起来更加地简单容易,因为它是专门为移动端设计的!

Termux - Android terminal and Linux environment

Termux结合了强大的终端仿真和广泛的Linux软件包集合,并且可以通过ROOT权限进行有意思的操作。

drizzleDumper - 内存搜索脱壳工具

drizzleDumper是一款基于内存搜索的Android脱壳工具,可用来脱360安全加固的壳。作者:DrizzleRisk

下载地址: http://pan.baidu.com/s/1hr49HBI 密码: 6bgv

jadx - Dex to Java decompiler

用于从Android Dex和Apk文件生成Java源代码的命令行和GUI工具(反编译)。

MT管理器 - ROOT文件管理器

可以使用ROOT权限复制文件等操作。

🌐重要包文

我是安卓手机,使用的软件是HttpCanary,非常好用,可以买个高级版支持作者。由于我安卓手机已经Root,直接信任了假的根证书。若是非Root用户,请使用平行空间处理(根据软件引导即可)开启抓包后,依次进行登录系统易班校本化每日健康打卡,选择今日打卡任务,填写反馈,提交操作。

1、[重要]登录包

类别说明
路径https://mobile.yiban.cn/api/v2/passport/login
方式GET
URL参数account:手机号
URL参数passwd:RSA公钥加密后的密码
URL参数ct:2(不知道干啥的,可能是固定的)
URL参数v:4.7.4(APP版本号 可固定)
URL参数identity:0(不知道干啥的,经检测其他值也可以,只是不能为空而已)

其它参数经测试,皆可为空。

若登录成功,返回的是个人信息、学校信息、和access_token(重要)。

2、首页信息包(程序不需访问)

类别说明
路径https://mobile.yiban.cn/api/v3/home?access_token=61c944fa9cf4cc0b4d52988d2f9cf233
方式GET
URL参数access_token:登录包下发的access_token

在返回的data的hotApps里有个这样的信息记录着打卡入口的地址

1
2
3
4
5
6
7
8
9
{
"name": "易班校本化",
"icon": "http://yfs01.fs.yiban.cn/web/1BQ48Z",
"url": "http://f.yiban.cn/iapp7463?access_token=61c944fa9cf4cc0b4d52988d2f9cf233&v_time=158509617310884",
"hyaline": false,
"source": 2,
"app_id": 80440,
"app_type": 4
}

3、[重要]二次请求码获取包

上面的包的url数据比较重要,访问后会重定向到http://f.yiban.cn/iapp/index?act=iapp7463&v={token},再访问这个重定向,又会被重定向到https://c.uyiban.com/#/?verify_request={verify_request}&yb_uid=13192120,其中verify_request很重要,用来二次认证作请求码。

4、[重要]二次认证包

类别说明
路径https://api.uyiban.com/base/c/auth/yiban
方式GET
URL参数verifyRequest:认证请求码,也就是刚刚的verify_request
URL参数CSRF:Cross-site request forgery,防跨站请求伪造的token。本来应该是服务端下发,经检测,这里是客户端随机生成的字符串,可以随便填东西。服务端会记住这个东西,所以以后的请求也要提交一样的字符串
Cookiescsrf_token:刚刚客户端生成的字符串

若请求成功,会返回个人的信息,下发了2个Cookies,以后的请求都要带上,其中一个是PHPSESSID,一个是cpi。其中cpi可以base64解码:信息为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"Channel": "yiban",
"Expire": 0,
"Client": "c",
"PersonId": "假装打码",
"UniversityName": "长沙理工大学",
"City": "长沙市",
"PersonType": "student",
"Grade": 2017,
"Gender": 1,
"EducationLevel": 2,
"Timestamp": 1581782834,
"Token": "假装打码"
}

好像也没啥用,主要是是PHPSESSID记录了会话信息。

5、未完成打卡清单

类别说明
路径https://api.uyiban.com/officeTask/client/index/uncompletedList
方式GET
URL参数CSRF:刚刚的随机字符串
Cookiescsrf_token:刚刚生成的随机值;还有刚刚下发的2个Cookies

返回如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"code": 0,
"msg": "",
"data": [
{
"TaskId": "假装打码",
"OrgId": "",
"TimeoutState": 1,
"Title": "2月15日长沙理工大学健康打卡拟登记表",
"Type": 1,
"StartTime": 1581696000,
"EndTime": 1581782340
}
]
}

其中重要的是TaskId任务Id等下要提交的。其他的不怎么用得到,TimeoutState是是否超时的一个状态码,Type不清楚,此外还有开始时间和截止时间等信息。

6、任务详细

类别说明
路径https://api.uyiban.com/officeTask/client/index/detail
方式GET
URL参数CSRF:刚刚的随机字符串
URL参数TaskId:具体打卡任务的Id
Cookiescsrf_token:刚刚生成的随机值;还有刚刚下发的2个Cookies

返回如下JSON:

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
{
"code": 0,
"msg": "",
"data": {
"Id": "假装打码",
"Title": "2月15日长沙理工大学健康打卡拟登记表",
"Type": 1,
"TypeId": "2846c4086bc70b93ce3295800b764fb1",
"PubOrgName": "学生工作部",
"PubPersonName": "曹婷",
"Content": "<p>请同学们认真填写,在疫情面前,每个人都能够出一份力!</p>",
"AttachmentIds": "",
"PubOrgId": "0e70b45b68bfd3c42c9cfe7440721b31",
"StartTime": 1581696000,
"TimeState": 1,
"WFId": "d336f36226e932d152ed54cc2a1baf20",
"AttachmentList": [],
"WorkflowState": 0,
"RetreatCount": 0,
"InitiateId": "",
"RetreatReason": "",
"EvaluationState": 0,
"EvaluationReason": "",
"State": "uncompleted"
}
}

其中最重要的是WFId,经研究,这个是固定的表单Id,也就是说只要我们要填的表单内容不变,这个值永远不变,经发现从试运行到现在一直是同一份表单。记录下固定的WFId就好。其中的TitlePubOrgNamePubPersonName提交打卡表单的时候要用到。

7、表单详细(程序不需访问)

类别说明
路径https://api.uyiban.com/workFlow/c/my/form/{WFId}
方式GET
路径占位符参数WFId:固定的疫情表单Id
URL参数CSRF:刚刚的随机字符串
Cookiescsrf_token:刚刚生成的随机值;还有刚刚下发的2个Cookies

这个对程序来说更不重要了,就是获取表单里面要填的东西。对返回的东西进行二次解析后结果是:

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
[
{
"component": "Input",
"props": {
"label": "姓名",
"placeholder": "请输入",
"required": true
},
"id": "69d1ca628e017a2c182902bfabdabd42"
},
{
"component": "Input",
"props": {
"label": "专业班级",
"placeholder": "请输入",
"required": true
},
"id": "5680543d3631077265b049b7d9ae418e"
},
{
"component": "GdMap",
"props": {
"label": "家庭所在地(到县)",
"extra": "请选择",
"required": true,
"params": {
"keywords": "",
"zoom": 15,
"radius": 1000,
"total": 20
}
},
"id": "e62910f76e9d5ba63ddc84ae68606f0f"
},
{
"component": "GdMap",
"props": {
"label": "目前所在地(到县)",
"extra": "请选择",
"required": true,
"params": {
"keywords": "",
"zoom": 15,
"radius": 1000,
"total": 20
}
},
"id": "ba7cabc21493b23bcfd65fa79525c4e0"
},
{
"component": "Input",
"props": {
"label": "当前体温",
"placeholder": "℃",
"required": true
},
"id": "cf4bac544816ca83db09a7d8c4d69178"
},
//省略……………………………………………………………………
{
"component": "Radio",
"props": {
"label": "2周内是否接触过确诊病例",
"extra": "请选择",
"options": [
"是",
"否",
"情况下方补充说明"
],
"required": true
},
"id": "a6288aa438a4e6e9264f029cc8dc5a5d"
},
{
"component": "Textarea",
"props": {
"label": "是否接触过确诊病例补充",
"placeholder": "请输入",
"required": false
},
"id": "a34eef03f5588c95241cb3bdf30e025c"
}
]

其中id就是具体要填写的内容的idrequired是这项是否必填。

8、[重点]表单提交

类别说明
路径https://api.uyiban.com/workFlow/c/my/apply/{WFId}
方式POST
路径占位符参数WFId:固定的疫情表单Id
URL参数CSRF:刚刚的随机字符串
URL参数TaskId:具体打卡任务的Id
Cookiescsrf_token:刚刚生成的随机值;还有刚刚下发的所有Cookies

POST BODY有两个:
data:表单具体内容,如下:

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
{
"69d1ca628e017a2c182902bfabdabd42": "姓名",
"5680543d3631077265b049b7d9ae418e": "班级",
"e62910f76e9d5ba63ddc84ae68606f0f": {
"name": "地址名",
"location": "经度,纬度",//注:精确到小数点6位
"address": "详细地址"
},
"ba7cabc21493b23bcfd65fa79525c4e0": {
"name": "地址名",
"location": "经度,纬度",//注:精确到小数点6位
"address": "详细地址"
},
"cf4bac544816ca83db09a7d8c4d69178": "温度",
"f16558084d32bee1523e085c9be35c30": "无",
"78bb535617d4caf28944bff53f434e32": "无",
"43cfde1796a98708e3df57f8088460e4": "无",
"8f472f4a665f93acf3de5c4ecab8c213": "无",
"e8578087affe7bde28eb5b6ffa5149e1": "否",
"24d085dd92e3a2bf43fef782e1fc7025": "否",
"bd397e1b6437a9dc6129db60d82ffd02": "否",
"d5adcefa1558c2759edd7c1cb41afbc4": "健康描述",
"484b372a88bb52cc0c54dcfbe618f779": "家庭健康描述",
"f0ac1554f16879b966c2135bcf3bdb53": "否",
"7b771dd1f3512486fac560cfec00052b": "否",
"a6288aa438a4e6e9264f029cc8dc5a5d": "否"
}

若你要使用,直接修改上方的一些数据即可。
extend,是传一些自定义信息,经检测,应该是固定的三个信息:任务名称、发布机构、发布人。这三个信息可以在第4个包里找到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"TaskId": "假装打码",
"title": "任务信息",
"content": [
{
"label": "任务名称",
"value": "3月25日长沙理工大学健康打卡登记表"
},
{
"label": "发布机构",
"value": "学生工作部"
},
{
"label": "发布人",
"value": "长沙理工大学"
}
]
}

正确的话,请求返回的code是整数0,并且会返回initiateId用于转发表单。

9、获取转发表单URL

类别说明
路径https://api.uyiban.com/workFlow/c/work/share?InitiateId=%s&CSRF=%s
方式GET
URL参数CSRF:刚刚的随机字符串
URL参数InitiateId:刚刚打卡返回的
Cookiescsrf_token:刚刚生成的随机值;还有刚刚下发的所有Cookies

可以得到人人可以访问的表单浏览URL

😍源码

util.py

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
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_v1_5
import base64
from urllib import parse

PUBLIC_KEY = '''-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAxbzZk3gEsbSe7A95iCIk
59Kvhs1fHKE6zRUfOUyTaKd6Rzgh9TB/jAK2ZN7rHzZExMM9kw6lVwmlV0VabSmO
YL9OOHDCiEjOlsfinlZpMZ4VHg8gkFoOeO4GvaBs7+YjG51Am6DKuJWMG9l1pAge
96Uhx8xWuDQweIkjWFADcGLpDJJtjTcrh4fy8toE0/0zJMmg8S4RM/ub0q59+VhM
zBYAfPmCr6YnEZf0QervDcjItr5pTNlkLK9E09HdKI4ynHy7D9lgLTeVmrITdq++
mCbgsF/z5Rgzpa/tCgIQTFD+EPPB4oXlaOg6yFceu0XUQEaU0DvAlJ7Zn+VwPkkq
JEoGudklNePHcK+eLRLHcjd9MPgU6NP31dEi/QSCA7lbcU91F3gyoBpSsp5m7bf5
//OBadjWJDvl2KML7NMQZUr7YXqUQW9AvoNFrH4edn8d5jY5WAxWsCPQlOqNdybM
vKF2jhjIE1fTWOzK+AvvFyNhxer5bWGU4S5LTr7QNXnvbngXCdkQfrcSn/ydQXP0
vXfjf3NhpluFXqWe5qUFKXvjY6+PdrE/lvTmX4DdvUIu9NDa2JU9mhwAPPR1yjjp
4IhgYOTQL69ZQcvy0Ssa6S25Xi3xx2XXbdx8svYcQfHDBF1daK9vca+YRX/DzXxl
1S4uGt+FUWSwuFdZ122ZCZ0CAwEAAQ==
-----END PUBLIC KEY-----
'''


def encrypt_passwd(pwd):
cipher = PKCS1_v1_5.new(RSA.importKey(PUBLIC_KEY))
cipher_text = base64.b64encode(cipher.encrypt(bytes(pwd, encoding="utf8")))
return parse.quote(cipher_text.decode("utf-8"))

yiban.py

注:下面代码IMG_SAVE_PATHINFO_PATH分别是截图保存目录和帐号密码保存目录,INFO_PATH有两个文件,一个是account.txt,存帐号和密码,以空格隔开,一行一个帐号密码。data.txt存抓到的提交表单包的数据,一行一个

截图功能需安装谷歌浏览器和谷歌驱动chromedriver,下载地址:http://chromedriver.storage.googleapis.com/index.html

选择对应的浏览器版本和操作系统下载,下载解压后放到INFO_PATH文件夹下,如果你是windows系统的话chrome_driver = INFO_PATH + "chromedriver" 这句话应该改为

chrome_driver = INFO_PATH + "chromedriver.exe"

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
# coding:utf-8
import re
import time
import traceback

import requests
import json
from selenium.webdriver.chrome.options import Options
from selenium import webdriver
from util import encrypt_passwd

IMG_SAVE_PATH = "data/" # 截图目录
INFO_PATH = "data/" # 帐号密码目录
chrome_options = Options()
chrome_options.add_argument('--no-sandbox')
chrome_options.add_argument('--disable-dev-shm-usage')
chrome_options.add_argument('window-size=500x1300') # 指定浏览器分辨率
chrome_options.add_argument('--disable-gpu') # 谷歌文档提到需要加上这个属性来规避bug
chrome_options.add_argument('--headless') # 浏览器不提供可视化页面. linux下如果系统不支持可视化不加这条会启动失败
chrome_driver = INFO_PATH + "chromedriver" # 自行下载驱动后 放入这个目录 是windows的话写chromedriver.exe

driver = webdriver.Chrome(executable_path=chrome_driver, chrome_options=chrome_options)


class YiBan:
WFId = "d336f36226e932d152ed54cc2a1baf20" # 疫情表单:固定表单值固定 无需更改
CSRF = "sui-bian-fang-dian-dong-xi" # 随机值 随便填点东西
COOKIES = {"csrf_token": CSRF} # 固定cookie 无需更改
HEADERS = {"Origin": "https://c.uyiban.com", "User-Agent": "yiban"} # 固定头 无需更改

def __init__(self, account, passwd):
self.account = account
self.passwd = passwd
self.session = requests.session()

def request(self, url, method="get", params=None, cookies=None):
if method == "get":
req = self.session.get(url, params=params, timeout=10, headers=self.HEADERS, cookies=cookies)
else:
req = self.session.post(url, data=params, timeout=10, headers=self.HEADERS, cookies=cookies)
try:
print(req.json())
return req.json()
except:
return None

def login(self):
params = {
"account": self.account,
"ct": 2,
"identify": 0,
"v": "4.7.4",
"passwd": encrypt_passwd(self.passwd)
}
r = self.request(url="https://mobile.yiban.cn/api/v2/passport/login", params=params)
if r is not None:
self.access_token = r["data"]["access_token"]
self.name = r["data"]["user"]["name"]
return r

def auth(self):
location = self.session.get("http://f.yiban.cn/iapp/index?act=iapp7463&v=%s" % self.access_token,
allow_redirects=False).headers["Location"]
verifyRequest = re.findall(r"verify_request=(.*?)&", location)[0]
print(verifyRequest)
return self.request(
"https://api.uyiban.com/base/c/auth/yiban?verifyRequest=%s&CSRF=%s" % (verifyRequest, self.CSRF),
cookies=self.COOKIES)

def getUncompletedList(self):
return self.request("https://api.uyiban.com/officeTask/client/index/uncompletedList?CSRF=%s" % self.CSRF,
cookies=self.COOKIES)

def getCompletedList(self):
return self.request("https://api.uyiban.com/officeTask/client/index/completedList?CSRF=%s" % self.CSRF,
cookies=self.COOKIES)

def getTaskDetail(self, taskId):
return self.request(
"https://api.uyiban.com/officeTask/client/index/detail?TaskId=%s&CSRF=%s" % (taskId, self.CSRF),
cookies=self.COOKIES)

def submit(self, data, extend):
params = {
"data": data,
"extend": extend
}
return self.request(
"https://api.uyiban.com/workFlow/c/my/apply/%s?CSRF=%s" % (self.WFId, self.CSRF), method="post",
params=params,
cookies=self.COOKIES)

def getShareUrl(self, initiateId):
return self.request(
"https://api.uyiban.com/workFlow/c/work/share?InitiateId=%s&CSRF=%s" % (initiateId, self.CSRF),
cookies=self.COOKIES)


if __name__ == '__main__':
with open(INFO_PATH + "account.txt", encoding="utf-8") as f:
allAccount = f.read().splitlines()
for i, v in enumerate(allAccount):
allAccount[i] = v.split()
with open(INFO_PATH + "data.txt", encoding="utf-8") as f:
allData = f.read().splitlines()

print("++++++++++%s++++++++++" % time.strftime("%Y-%m-%d %H:%M:%S"))

for index, account_detail in enumerate(allAccount):
try:
yb = YiBan(account_detail[0], account_detail[1])
yb.login()
result_auth = yb.auth()
data_url = result_auth["data"].get("Data")
if data_url is not None: # 授权过期 20200411
print("授权过期")
print("访问授权网址")
result_html = yb.session.get(url=data_url, headers=yb.HEADERS,
cookies={"loginToken": yb.access_token}).text
re_result = re.findall(r'input type="hidden" id="(.*?)" value="(.*?)"', result_html)
print("输出待提交post data")
print(re_result)
post_data = {"scope": "1,2,3,"}
for i in re_result:
post_data[i[0]] = i[1]
print("进行授权确认")
usersure_result = yb.session.post(url="https://oauth.yiban.cn/code/usersure",
data=post_data,
headers=yb.HEADERS, cookies={"loginToken": yb.access_token})
if usersure_result.json()["code"] == "s200":
print("授权成功!")
else:
print("授权失败!")
continue
print("尝试重新二次登录")
yb.auth()
all_task = yb.getUncompletedList()
if len(all_task["data"]) == 0:
print("没有待完成的打卡任务")
for i in all_task["data"]:
task_detail = yb.getTaskDetail(i["TaskId"])["data"]
if task_detail["WFId"] != yb.WFId:
print("表单已更新,得更新程序了")
exit()
ex = {"TaskId": task_detail["Id"],
"title": "任务信息",
"content": [{"label": "任务名称", "value": task_detail["Title"]},
{"label": "发布机构", "value": task_detail["PubOrgName"]},
{"label": "发布人", "value": task_detail["PubPersonName"]}]}
submit_result = yb.submit(allData[index], json.dumps(ex, ensure_ascii=False))
if submit_result["code"] == 0:
share_url = yb.getShareUrl(submit_result["data"])["data"]["uri"]
driver.get(share_url)
driver.refresh()
time.sleep(5)
file_name = "%s%s.png" % (yb.name, time.strftime("%Y-%m-%d"))
driver.get_screenshot_as_file(IMG_SAVE_PATH + file_name)
print("已完成一次打卡,截图保存为:%s" % file_name, "链接为%s" % share_url)
else:
print("打卡失败,遇到了一些错误!")
print("-------------------------------------")
except:
traceback.print_exc()
print("遇到了一些错误~")
driver.quit()

😇如何自动?

买个服务器,使用Linux的crontab就好啦,加上如下规则:

1
1 0 * * * python3 /root/yiban.py >> /root/result.txt

这样就会每天的0点1分运行这个脚本并把输出追加到结果文件啦。每天看看结果就行了,还想折腾的,可以搞个自动发邮件通知自己即可。

运行截图:
图1:打卡运行截图
我把自动保存的截图证明又搭了个站点,这样每天只要访问站点就能获取到当天的截图啦!
图2:打卡站点截图