不会有人因此受伤
脚本对您做了什么
- 使用统一身份认证的用户名和密码, 帮您登录
- 查看您的健康打卡记录
- 将上一次成功的打卡记录复制, 作为今天的打卡记录
脚本对健康打卡做了什么
- 脚本将“每日健康打卡”转为了“健康变动申报”
- 不用脚本: 全量打卡
- 无论您的健康状况有无变化, 都需要每天填写全部的信息
- 使用脚本: 差异打卡
- 如果健康状况没有变化, 那么您无需做任何事, 脚本将产生与实际相符的结果
- 如果有变化, 您可以在脚本完成后, 手动打卡一次, 从此以后脚本将帮您填写新的状况, 依然与实际相符
- 推荐阅读
免责声明
- 下面的教程仅供计算机网络与软件工程方向的某校学生学习研究, 不作为任何其他用途
- 若进行除学习研究外的任何操作, 后果由使用者自行承担, 与本文作者无关
每日健康打卡是怎么工作的
整体流程
- 统一身份认证登录, 取得关键cookie:
MOD_AUTH_CAS
- 查询历史打卡, 找到数据
- 用历史打卡的数据, 完成当天的打卡
统一身份认证
- 链接:
http://authserver.nju.edu.cn/authserver/login
- 请求
GET
: 获得登录网页POST
: 登录request.data
: application/x-www-form-urlencoded
name | comment | source |
---|
username | 用户名(明文) | 用户输入 |
password | 密码(密文, AES加密) | 用户输入 -> JS计算 |
lt | CAS Login Ticket | body |
dllt | CAS 相关神秘参数 | body |
execution | CAS 相关神秘参数 | body |
_eventId | CAS 相关神秘参数 | body |
rmShown | CAS 相关神秘参数 | 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
部署
依赖
requests
beautifulsoup4
pycryptodome
平台
- serverless的云函数平台
- Azure Functions: 提供有限技术支持, 并且在这里有完成的代码
Github
- 腾讯云SCF: 测试可用, 不提供技术支持
- AWS Lambda等, 未测试, 不提供技术支持
- 自建服务器并使用crontab