前言

快开学了怎么还是没有动静捏,让天天打卡哩,整一个自动打卡节省自己的时间,写完之后在这里记录一下思路供各位参考

滑稽你我他

思路

概述

学校使用耶鲁大学的 CAS 来做 SSO 统一身份认证,调整了 CAS 的默认加密方法,有点复杂,前端看不懂啦(看了一下,加盐了,而且可能拼接了一个字段上去,没看出来具体是哪种方式,应该是SHA系列的加密),所以打算用 Chrome 模拟点击的方式来登录系统,拿到认证 Code 或者直接去填写

旧版

旧版做起来比较简单,因为有 WEB 版的,就直接在 WEB 上填写,Go 的话有个 chromedp 库可以调用并操作一个无头(Headless)的 Chrome 浏览器,通过 HTML 的选择器可以选择相应元素并进行操作,登录那里选择用户名和密码的框框,然后把用户的学工号和密码输入进去敲个回车即可完成登录流程,需要特别注意的是,用程序操作时浏览器不会自动跳转到登录后页面,需要手动跳转一下到 OA 首页,就可以直接进入,不会再跳回到 CAS 页面。接下来直接跳转到旧版填报页面填报即可

新版

新版的情况比较复杂,通过 Fiddler 抓包可以发现新版只能在 APP(Android 12就打不开了,需要用虚拟机) 或者微信进入到那个页面进行打卡,而打卡的定位是调用微信接口走高德地图去获取的位置,前端做了校验限制,不能手动填地址了,所以在旧版中全程使用 模拟点击完成打卡的方法就无效了,也就是说只能走 APP 或者微信的方法。

稍微卡了几个小时,抓了个包之后就豁然开朗:可以不走 CAS 的微信登录接口,使用正常方式登录之后拿到用户信息之后直接向这个数据接口发送请求上传数据。

抓提交的 saveandsubmit 这个包可以发现提交的接口长这个样子

{
"id":"xxxxxxxxxxxxx",
"formData":{
"JZDZ":"xx省/xx市/xx区/xx街道/xxxx",
"CWJTWSFZC":"是",
"SFBGL":"option-1",
"SFYYSGRQK":"option-2"
}
}

通过分析,不难看出,这个 id 就是用来识别表单的,这个 formData 是表格里面需要填写的内容, JZDZ 是居住地址的简写, CWJTWSFZC 是晨午检体温是否正常,SFBGL 是是否被集中隔离:1是“否”,2是“是”,SFYYSGRQK 是是否有核酸证明,2是“有”,1是“无”

提交的 Header 带了一堆参数,里面既有 Session 又有 Token ,实际上需要的其实只有一个 Authorization 带上这个参数就能通过认证,拿到数据,正常提交的数据返回是这样。

{
"message" : "提交成功",
"data" : null,
"errorMessage" : null
}

那我们怎么知道要提交的 id 是什么呢,观察查询当前所有打卡的 queryListForPage 请求,就可以知道啦,这个请求发送这样的数据:

{"pageNum":1,"pageSize":10,"pageParam":{"name":"22-09-05"}}

其中,只需要关注 pageParam 参数,我们可以用这个参数查询当日数据,然后从这个返回拿到当日的 id ,发送一下,得到了这样的返回

[
{
"id": "xxxxxxxxxxxxxxxxxx",
"userId": "xxxxxxxxxxxxxxxx",
"taskId": "xxxxxxxxxxxx",
"taskCreateUserName": "管理员",
"taskName": "2022-09-05-学生每日健康数据填报",
"formId": "xxxxxxx",
"content": null,
"formData": {
"CWJTWSFZC": "是",
"userInfo": null,
"JZDZ": "xx省/xx市/xx区/xx街道/xxxx",
"SFBGL": "option-1",
"userInfo3": null,
"SFYYSGRQK": "option-2",
"userInfo2": null,
"userInfo1": null
},
"state": 2,
"submitTime": "xxxx",
"createDate": "xxxx"
}
]

乍一看是不是觉得 formId 是不是就是上面我们需要的 id ?啊哈,其实是 id 这个值

剩下需要注意的参数是 state ,0 是未填,1 是已保存,2 是已保存并提交,可以用这个来判断提交情况

另外,这个 json 不是很标准,用 Go 处理的时候需要记得把前后的“[]”去掉再来处理

现在整体思路就很明了了:

使用 chromedp 模拟登录->拿到 Authorization Code->查询当日表格 id ->使用接口上传数据

新版思路详解

使用 chromedp 模拟登录

和旧版的一样,非常简单不再详述

拿到 Authorization Code

应该说是最大的难点了,自己用浏览器的话,可以很轻松的从 Dev Tools(也就是 F12 ) 看到请求的 Header ,看到里面的 Authorization 字段,但是怎么让程序拿到呢

所幸,谷歌提供了 CDP(Chrome DevTools Protocol) 开放了 Dev Tools 里面的内容给第三方程序,具体到 Go 的话,可以用 cdproto 这个库来拿里面的内容

但是 Chrome DevTools Protocol 这块能找到的参考资料基本上都没有(中英文都是),所以基本上只能硬啃它的文档(这个),只有英文

我们发现 Authorization 字段登录后在每日填报的 queryListForPage 请求中可以拿到,在 Chrome 的 Dev Tools 中,它是在 Network 中的,所以我们重点关注文档中 Network 的部分,仔细看文档,发现我们可以通过“筛选请求方式->筛选URL”的方式拿到这个请求,在 Dev Tools 打卡设置,打开“协议浏览器”(Protocol Monitor)的实验性选项,我们可以发现 queryListForPage 这个请求的协议叫做 RequestWillBeSent ,我们筛选所有使用 RequestWillBeSent 方法请求的 URL,从里面找到含 queryListForPage 的 URL请求,就可以拿到这个具体请求的详细情况了,这里的具体代码可以参考新版打卡代码中的 func getCodeAndDataFill(ctx context.Context){} 方法

查询当日表格 id ->使用接口上传数据

拿到 Code 之后就很好办了,先调用 queryListForPage 接口(POST 方式)查询当日 ID,再用 PUT 方式调用 saveandsubmit 接口上传打卡数据,需要注意 Body 都是用 application/json 提交,Go 的话需要定义一个结构体来转 json

GitHub Action 运行的注意事项

GitHub Action 的 Secret 不能填中文,会乱码,所以需要编码,可以采用的 Unicode、URL 编码等,为了方便,我这里用了 URL 编码

GitHub 上巨硬的机器时区是 UTC,而我们需要获取北京时间的日期,用下面这个方法时,time.LoadLocation() 会报错,在目标机器上安装 Go 环境即可解决这个问题

timeLocation, _ := time.LoadLocation("Asia/Shanghai")
timestamp := time.Now().In(timeLocation)
nowdate := strftime.Format("%y-%m-%d", timestamp)
log.Println("今日日期:", nowdate)

结语

希望这篇文章能够帮助未来的金院学子开发类似程序,提升自动化程度,合鲤偷懒,提升效率bighuaji