注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、[重要]登录包

(已于20201119更新)感谢B站用户破损的鞘翅提供的好用并且简洁的接口。

类别说明
路径https://mobile.yiban.cn/api/v3/passport/login
方式GET
URL参数mobile:手机号
URL参数password:明文密码
URL参数imei:设备识别号,可以乱填

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

2、首页信息包(程序可以不访问写死iapp就好)

类别说明
路径https://mobile.yiban.cn/api/v3/home
方式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=假装打码token&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

😍源码

20201117更新,请打开https://github.com/looyeagee/yiban_auto_submit 查看详情

main.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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import json
import random
import time
import traceback
from yiban import YiBan
import util
if __name__ == '__main__':
try:
yb = YiBan("phone", "password") # FIXME:账号密码
yb.login()
yb.getHome()
print("登录成功 %s"%yb.name)
yb.auth()
all_task = yb.getUncompletedList()["data"]
all_task = list(filter(lambda x: "体温检测" in x["Title"], all_task)) # FIXME: 长理的打卡任务标题均含有"体温检测"字样 此举是防止其他表单干扰 (可能会变)
if len(all_task) == 0:
print("没找到今天长理体温上报的任务,可能是你已经上报,如果不是请手动上报。")
else:
all_task_sort = util.desc_sort(all_task, "StartTime") # 按开始时间排序
new_task = all_task_sort[0] # 只取一个最新的
print("找到未上报的任务:", new_task)
task_detail = yb.getTaskDetail(new_task["TaskId"])["data"]
ex = {"TaskId": task_detail["Id"],
"title": "任务信息",
"content": [{"label": "任务名称", "value": task_detail["Title"]},
{"label": "发布机构", "value": task_detail["PubOrgName"]},
{"label": "发布人", "value": task_detail["PubPersonName"]}]}
# FIXME: 以下是长沙理工大学最新的表单信息,由于某些值(检测时间)必须是动态的,所以只能将form表单写死在这里 (可能会变)
dict_form = {"2fca911d0600717cc5c2f57fc3702787": ["湖南省", "长沙市", "天心区"],
"cab886bf693f23a34ed78ed71deaadc3": yb.name,
"b418fa886b6a38bdce72569a70b1fa10": ["36.2", "36.3", "36.4", "36.5", "36.6", "36.7", "36.8"][random.randint(0, 6)], # 随机体温
"c77d35b16fb22ec70a1f33c315141dbb": util.get_time_no_second()}
submit_result = yb.submit(json.dumps(dict_form, ensure_ascii=False), json.dumps(
ex, ensure_ascii=False), task_detail["WFId"])
if submit_result["code"] == 0:
share_url = yb.getShareUrl(submit_result["data"])["data"]["uri"]
print("已完成一次体温上报[%s]" % task_detail["Title"])
print("访问此网址查看详情:%s" % share_url)
else:
print("[%s]遇到了一些错误:%s" % (task_detail["Title"], submit_result["msg"]))
except Exception as e:
print("出错啦")
print(e)

yiban.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
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
import re
import requests
import util
class YiBan:
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()
self.name = ""
self.iapp = ""

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:
return req.json()
except:
return None

def login(self):
params = {
"mobile": self.account,
"imei": "0",
"password": self.passwd
}
# 最新不需要加密密码直接登录的接口来自我B站视频评论用户:破损的鞘翅(bilibili_id:45807603)
r = self.request(url="https://mobile.yiban.cn/api/v3/passport/login", params=params)
if r is not None and str(r["response"]) == "100":
self.access_token = r["data"]["user"]["access_token"]
return r
else:
raise Exception("账号或密码错误")
def getHome(self):
params = {
"access_token": self.access_token,
}
r = self.request(url="https://mobile.yiban.cn/api/v3/home", params=params)
self.name = r["data"]["user"]["userName"]
for i in r["data"]["hotApps"]: # 动态取得iapp号 20201117更新
if i["name"] == "易班校本化":
self.iapp = re.findall(r"(iapp.*)\?", i["url"])[0]
return r
def auth(self):
params = {
"act": self.iapp,
"v": self.access_token
}
print()
location = self.session.get("http://f.yiban.cn/iapp/index",params=params,
allow_redirects=False).headers.get("Location")
if location is None:
raise Exception("该用户可能没进行校方认证,无此APP权限")
verifyRequest = re.findall(r"verify_request=(.*?)&", location)[0]
result_auth = self.request(
"https://api.uyiban.com/base/c/auth/yiban?verifyRequest=%s&CSRF=%s" % (verifyRequest, self.CSRF),
cookies=self.COOKIES)
data_url = result_auth["data"].get("Data")
if data_url is not None: # 授权过期
result_html = self.session.get(url=data_url, headers=self.HEADERS,
cookies={"loginToken": self.access_token}).text
re_result = re.findall(r'input type="hidden" id="(.*?)" value="(.*?)"', result_html)
post_data = {"scope": "1,2,3,"}
for re_i in re_result:
post_data[re_i[0]] = re_i[1]
usersure_result = self.session.post(url="https://oauth.yiban.cn/code/usersure",
data=post_data,
headers=self.HEADERS, cookies={"loginToken": self.access_token})
if usersure_result.json()["code"] == "s200":
return self.auth()
else:
return False
else:
return True

def getUncompletedList(self):
params = {
"CSRF": self.CSRF,
"StartTime": util.get_today(),
"EndTime": util.get_time()
}
return self.request("https://api.uyiban.com/officeTask/client/index/uncompletedList", params=params,
cookies=self.COOKIES)

def getCompletedList(self):
params = {
"CSRF": self.CSRF,
"StartTime": util.get_7_day_ago(),
"EndTime": util.get_time()
}
return self.request("https://api.uyiban.com/officeTask/client/index/completedList", params=params,
cookies=self.COOKIES)

def getJsonByInitiateId(self, initiate_id):
params = {
"CSRF": self.CSRF
}
return self.request("https://api.uyiban.com/workFlow/c/work/show/view/%s" % initiate_id, params=params,
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, wfid):
params = {
"data": data,
"extend": extend
}
return self.request(
"https://api.uyiban.com/workFlow/c/my/apply/%s?CSRF=%s" % (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)

😇如何自动?

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

1
1 0 * * * python3 /root/yiban/main.py

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

运行截图(老版本留个纪念)
图1:打卡运行截图