Featured image of post NJU Health Checkin

NJU Health Checkin

某高校每日健康打卡的自动化

不会有人因此受伤

脚本对您做了什么

  • 使用统一身份认证的用户名和密码, 帮您登录
  • 查看您的健康打卡记录
  • 将上一次成功的打卡记录复制, 作为今天的打卡记录

脚本对健康打卡做了什么

  • 脚本将“每日健康打卡”转为了“健康变动申报”
  • 不用脚本: 全量打卡
    • 无论您的健康状况有无变化, 都需要每天填写全部的信息
  • 使用脚本: 差异打卡
    • 如果健康状况没有变化, 那么您无需做任何事, 脚本将产生与实际相符的结果
    • 如果有变化, 您可以在脚本完成后, 手动打卡一次, 从此以后脚本将帮您填写新的状况, 依然与实际相符
  • 推荐阅读

免责声明

  • 下面的教程仅供计算机网络与软件工程方向的某校学生学习研究, 不作为任何其他用途
  • 若进行除学习研究外的任何操作, 后果由使用者自行承担, 与本文作者无关

每日健康打卡是怎么工作的

整体流程

  1. 统一身份认证登录, 取得关键cookie: MOD_AUTH_CAS
  2. 查询历史打卡, 找到数据
  3. 用历史打卡的数据, 完成当天的打卡

统一身份认证

  • 链接: http://authserver.nju.edu.cn/authserver/login
  • 请求
    • GET: 获得登录网页
      • response.body: 登录页面
    • POST: 登录
      • request.data: application/x-www-form-urlencoded
        namecommentsource
        username用户名(明文)用户输入
        password密码(密文, AES加密)用户输入 -> JS计算
        ltCAS Login Ticketbody
        dlltCAS 相关神秘参数body
        executionCAS 相关神秘参数body
        _eventIdCAS 相关神秘参数body
        rmShownCAS 相关神秘参数body
  • 加密
    • 请看下代码
      • 顶层: authserver.nju.edu.cn/authserver/custom/js/login-wisedu_v1.0.js
        // 帐号登陆提交banding事件
        var casLoginForm = $("#casLoginForm");
        casLoginForm.submit(doLogin);
        function doLogin() {
            ...
            var password = casLoginForm.find("#password");
            ...
            _etd2(password.val(),casLoginForm.find("#pwdDefaultEncryptSalt").val());
        }
        // 压成一行的函数, 用于调用加密, 并将结果放到<input id='passwordEncrypt'>
        function _etd2(_p0, _p1) {
            try {
                var _p2 = encryptAES(_p0, _p1);
                $("#casLoginForm").find("#passwordEncrypt").val(_p2);
            } catch (e) {
                $("#casLoginForm").find("#passwordEncrypt").val(_p0);
            }
        }
        
      • 加密: authserver.nju.edu.cn/authserver/custom/js/encrypt.js
        // 加密顶层, 检查空串, 设置参数
        function encryptAES(data, _p1) { 
            if (!_p1) { 
                return data; 
            } 
            var encrypted = _gas(_rds(64) + data, _p1, _rds(16)); 
            return encrypted; 
        }
        // 加密本体, encode然后执行加密, 使用CryptoJS
        function _gas(data, key0, iv0) { 
            key0 = key0.replace(/(^\s+)|(\s+$)/g, ""); 
            var key = CryptoJS.enc.Utf8.parse(key0); 
            var iv = CryptoJS.enc.Utf8.parse(iv0); 
            var encrypted = CryptoJS.AES.encrypt(data, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); 
            return encrypted.toString(); 
        }
        // 从chars生成随机字符串, 参数为长度
        var $_chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678'; 
        var _chars_len = $_chars.length;
        function _rds(len) { 
            var retStr = ''; 
            for (i = 0; i < len; i++) { 
                retStr += $_chars.charAt(Math.floor(Math.random() * _chars_len)); 
            } 
            return retStr; 
        }
        
    • 流程(没必要说了)
      • login-wisedu_v1.0.js监听submit()事件, 绑定到doLogin()
      • 用户输入密码并点击登录按钮, 触发submit事件
      • doLogin()处理事件, 执行_etd2()
      • _etd2()调用encryptAES()
      • encryptAES()调用_rds()制造随机的前导随机字符串和AES初始向量, 调用_gas()
      • _gas()调用CryptoJS.AES.encrypt()执行真正的加密, 算法为CBC
    • 可以想着玩的问题
      • 前导随机串有什么用?
      • 初始向量没有传递给后端, 那么后端怎么正确解密呢? 或者说后端为什么不需要初始向量就能解密?
  • 代码: 用Python重现加密, 仅使用requests实现登录
# 代码风格不是很好, 您看之前可以先吃点降压药
# 其实_rds和_gas都可以一行代码写完的🤣, 这里分行主要是保留js代码中的关键变量, 便于对照
# exception处理和debug信息打印, 请根据部署的平台自行修改

# encrypt func
def encryptAES(_p0: str, _p1: str) -> str:
    def _rds(len: int) -> str: 
        _chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678'
        return ''.join(random.choices(_chars, k=len))
    def _gas(data: str, key0: str, iv0: str) -> bytes:
        encrypt = AES.new(key0.strip().encode('utf-8'), AES.MODE_CBC, iv0.encode('utf-8'))
        return base64.b64encode(encrypt.encrypt(Padding.pad(data.encode('utf-8'), 16)))
    return _gas(_rds(64) + _p0, _p1, _rds(16)).decode('utf-8')

# const 
username = os.environ['NU_USER']
password = os.environ['NU_PASS']
url_login = r'https://authserver.nju.edu.cn/authserver/login'
url_list = r'http://ehallapp.nju.edu.cn/xgfw/sys/yqfxmrjkdkappnju/apply/getApplyInfoList.do'
url_apply = r'http://ehallapp.nju.edu.cn/xgfw/sys/yqfxmrjkdkappnju/apply/saveApplyInfos.do'
session = requests.Session()

# login
response = session.get(url_login)

if response.status_code == 200:
    logging.info('Open login page: %d, %s' % (response.status_code, response.reason))
else:
    logging.error('Open login page: %d, %s' % (response.status_code, response.reason))
    return

soup = BeautifulSoup(response.text, 'html.parser')
soup.select_one("#pwdDefaultEncryptSalt").attrs['value']
data_login = {
    'username':     username, 
    'password':     encryptAES(password, soup.select_one("#pwdDefaultEncryptSalt").attrs['value']),
    'lt' :          soup.select_one('[name="lt"]').attrs['value'], 
    'dllt' :        soup.select_one('[name="dllt"]').attrs['value'], 
    'execution' :   soup.select_one('[name="execution"]').attrs['value'], 
    '_eventId' :    soup.select_one('[name="_eventId"]').attrs['value'], 
    'rmShown' :     soup.select_one('[name="rmShown"]').attrs['value'], 
}

response = session.post(url_login, data_login)

if response.status_code == 200:
    logging.info('Login for %s: %d, %s' % (username, response.status_code, response.reason))
else:
    logging.error('Login for %s: %d, %s' % (username, response.status_code, response.reason))
    return

获取历史打卡

  • 链接: http://ehallapp.nju.edu.cn/xgfw/sys/yqfxmrjkdkappnju/apply/getApplyInfoList.do
  • 请求:
    • GET: 获得历史打卡
      • response.body: application/json
        {
            "code": "0", // 所以这里TM为什么是个字符串啊神经病吗
            "msg": "成功",
            "data": [
                {
                    // 一个打卡记录
                }
                ...
            ]
        }
        
  • 代码
# list
response = session.get(url_list)

try:
    content = response.json()
except ValueError:
    content = {}
if response.status_code == 200 and content.get('code') == '0':
    logging.info('List: %d, %s, %s' % (response.status_code, response.reason, content.get('msg') or 'No messgage available'))
else:
    logging.error('List: %d, %s, %s' % (response.status_code, response.reason, content.get('msg') or 'No messgage available'))
    return

进行当日打卡

  • 链接: http://ehallapp.nju.edu.cn/xgfw/sys/yqfxmrjkdkappnju/apply/saveApplyInfos.do
  • 请求
    • GET: 获得历史打卡
      • response.body: application/json
        {
            "code": "0", // 所以这里TM为什么是个字符串啊神经病吗
            "msg": "成功",
            "data": {}
        }
        
    • 所以TM为什么是GET啊神经病吗! 这不是我搞错了, 请您看看他的代码webpack:///TbChina.vue
      qd() {
          Get(api.saveApplyInfos, {
              WID: this.$route.query.item.WID,
              CURR_LOCATION: this.DD,
              IS_TWZC: this.value,
              IS_HAS_JKQK: this.value1,
              JRSKMYS: this.value2,
              JZRJRSKMYS: this.value3
          }).then(res => {
              if (res.code == "0") {
                  Toast("提交成功!");
                  history.back(-1);
              } else {
                  Toast(res.msg);
              }
          });
      },
      
  • 代码: 搜一个TJSJ(提交时间)有效的记录, 用当前的WID构造当日记录, 提交之
    • 请您不要问我为什么要这样判断, 我也不知道, 请看他的代码webpack:///China.vue, line 22
# apply
data = next(x for x in content['data'] if x.get('TJSJ') != '')
data_apply = {
    'WID':              content['data'][0]['WID'], 
    'CURR_LOCATION':    data['CURR_LOCATION'], 
    'IS_TWZC':          data['IS_TWZC'], 
    'IS_HAS_JKQK':      data['IS_HAS_JKQK'], 
    'JRSKMYS':          data['JRSKMYS'], 
    'JZRJRSKMYS':       data['JZRJRSKMYS']
}

 response = session.get(url_apply, params=data_apply)

try:
    content = response.json()
except ValueError:
    content = {}
if response.status_code == 200 and content.get('code') == '0':
    logging.info('Apply: %d, %s, %s' % (response.status_code, response.reason, content.get('msg') or 'No messgage available'))
else:
    logging.error('Apply: %d, %s, %s' % (response.status_code, response.reason, content.get('msg') or 'No messgage available'))
    return
pass

部署

依赖

  • requirements.txt
requests
beautifulsoup4
pycryptodome

平台

  • serverless的云函数平台
    • Azure Functions: 提供有限技术支持, 并且在这里有完成的代码 Github
    • 腾讯云SCF: 测试可用, 不提供技术支持
    • AWS Lambda等, 未测试, 不提供技术支持
  • 自建服务器并使用crontab
    • 测试可用, 不提供技术支持
Licensed under CC BY-NC-SA 4.0
Viewer Count: