河海大学每日自动健康打卡

Author Avatar
yiheng 1月22日
  • 在其它设备中阅读本文章

在2021年1月21日,河海大学终于放寒假了。快乐的寒假,怎么能被健康打卡系统所束缚呢,于是花了两个小时写了一个自动健康打卡的程序,现在记录一下编写历程。本程序全部使用kotlin语言,用到的库有okhttp与json

抓包分析

最开始,我本来以为这样一个健康打卡系统只需要简单的抓包发包就可以实现,但事实并不是这样。

打卡请求

可以看到,打卡的请求是如下一个post请求,其地址为 http://form.hhu.edu.cn/pdc/formDesignApi/dataFormSave?wid=?&userId=?

其中,wid是我们需要手动获取的

获取wid

那么我们应该如何获取wid呢,查看网页源代码可以发现,wid是后端直接传过来的一个字符串,在如下代码中

    $('title').html('本科生健康打卡');//动态设置title为表单名
    var _selfFormWid = 'XXXXXXXXX';
    var _fileID;//文件ID(用户文件存储路径)
    var _index = "true";//计数器(首次提交,清空文件数据)
    var _userId = 'XXXXXX';//用户ID

于是直接在获取到表单页面后用正则表达式解析

    val pattern = Pattern.compile("ar _selfFormWid = '(\w+)';")
    val matcher = pattern.matcher(form)
    var wid: String? = null
    while (matcher.find()) {
        wid = matcher.group(1)
    }

看上去很简单对不对,可是实际运行的时候永远找不到这个结果,打开chrome隐私模式分析发现,未登录的情况下会自动跳转到 http://ids.hhu.edu.cn/amserver/UI/Login?goto=http://form.hhu.edu.cn:80/pdc/form/list 这个页面进行登录。

那么接下来我们就需要进行登录。

登录

进行一次登录操作,轻轻松松抓到登录的请求

登录请求

可以在Header中看到中添加了几个Cookie。

于是我们需要保存下登录的Cookie。

首先先把登录的请求给构造出来

private fun login(id: String, pass: String) {
        val requestBody = FormBody.Builder()
            .add("IDToken1", id)
            .add("IDToken2", pass)
            .add("IDButton", "Submit")
            .add("goto", "aHR0cDovL2Zvcm0uaGh1LmVkdS5jbjo4MC9wZGMvZm9ybS9saXN0")
            .add("encoded", "true")
            .add("inputCode", "")
            .add("gx_charset", "UTF-8")
            .build()
        val request = Request.Builder()
            .addHeader("User-Agent", ua)
            .url(loginUrl)
            .post(requestBody)
            .build()
        client.newCall(request).execute()
}

然后查询文档得知okhttp进行Cookie操作需要自行实现CookieJar接口,于是随手写了个只能用于这个程序的CookieJar。

class CookieJarKt : CookieJar {
    var cookies: MutableList<Cookie>? = null

    override fun saveFromResponse(url: HttpUrl?, cookies: MutableList<Cookie>?) {
        if (cookies == null) return
        if (this.cookies == null) this.cookies = cookies
        if (url.toString() == "http://form.hhu.edu.cn/pdc/form/list") {
            this.cookies = cookies
        }
    }

    override fun loadForRequest(url: HttpUrl?): MutableList<Cookie>? {
        return cookies ?: mutableListOf()
    }

}

并构造出client对象

val client = OkHttpClient.Builder()
        .cookieJar(CookieJarKt())
        .build()

然后就可以成功获取到wid了

尝试自动打卡

获取到wid之后,就可以构造出打卡的请求然后进行打卡了。参数中需要日期:

fun getDate(): String {
    val simpleDateFormat = SimpleDateFormat("yyyy/MM/dd")
    return simpleDateFormat.format(Date())
}

然后先是完全按照我抓到的包实现了自动化,可以实现自动打卡,但是这样就只能我自己一个人使用了。想到健康打卡页面会自动填充,就想着能不能自动获取上一次打卡的结果来进行打卡呢

尝试获取上次打卡结果

根据我的经验,获取上次打卡结果应该是使用ajax进行的,在后端写了接口之后前端调用然后填充。然而读了一圈代码,并没有发现这么一个代码,反而发现了这个。

结果

天哪这是什么神仙操作,没有做前后端分离也太屑了吧,格式化解析了一下发现后端直接将之前的好几次结果放进了这个json,并且直接将打卡的请求的参数名对应上了。这样我们就只需要解析这两个json便可以获取数据直接提交

怎么感觉越来越简单了

提取一下之前获取wid的代码

fun findString(form: String,regex:String): String? {
    val pattern = Pattern.compile(regex)
    val matcher = pattern.matcher(form)
    var result: String? = null
    while (matcher.find()) {
        result = matcher.group(1)
    }
    return result
}

然后获取这两个参数

    private fun getDataDetail(form: String): JSONObject {
        val dataDetail: String = findString(form,"dataDetail = (.+),") ?: throw Exception("获取dataDetail失败")
        val jsonArray = JSONArray(dataDetail)
        return jsonArray.getJSONObject(0)
    }

    private fun getFillDetail(form: String): JSONObject {
        val fillDetail: String = findString(form,"fillDetail = (.+);") ?: throw Exception("获取fillDetail失败")
        val jsonArray = JSONArray(fillDetail)
        return jsonArray.getJSONObject(0)
    }

当然不要忘记在gradle中添加json依赖

    implementation group: 'org.json', name: 'json', version: '20201115'

这样就轻松获取到了打卡的请求。

本以为这样就万事大吉了,构造好请求之后却发现服务端直接给我抛了个异常,仔细观察后发现 fillDetail中有两个参数在请求中并没有出现,而且有的与dataDetail重复了,需要跳过。最终打卡代码如下

val excluded = arrayListOf("USERID", "CLRQ","XM_1474","XGH_336526","SFZJH_859173")
login(userId, password)
val form = getForm()
val wid = genWid(form)
val dataDetail = getDataDetail(form)
val fillDetail = getFillDetail(form)
val bodyBuilder = FormBody.Builder()
      .add("DATETIME_CYCLE", getDate())
for (s in dataDetail.keySet()) {
    bodyBuilder.add(s, dataDetail.getString(s))
}
for (s in fillDetail.keySet()) {
    val data = if (fillDetail.isNull(s)) "" else fillDetail.getString(s)
    if (s !in excluded) {
        bodyBuilder.add(s, data)
    }
}
val request = Request.Builder()
    .url("http://form.hhu.edu.cn/pdc/formDesignApi/dataFormSave?wid=${wid}&userId=${userId}")
    .addHeader("User-Agent", ua)
    .post(bodyBuilder.build())
    .build()
val response = client.newCall(request).execute()
if (!response.body().string().contains("true")) {
    throw Exception("打卡失败")
}

这样就做完了整个自动打卡的程序了。

封装

随手将打卡代码封装到一个类中,现在打卡就只需要构建出Daka(userId,password)对象,然后调用execute方法即可使用。

最终代码

你可以在gist上看到最终的代码

Daka.kt

utils.kt

CookieJarKt.kt

欢迎大家自行取用,如果信得过我的话也可以把账号密码给我我帮你挂着

    巡视官
    巡视官  2021-09-06, 13:36

    钱塘风雨起苍黄,贺鸡儿楠过大江