前言
先前有一个契机,需要模拟登陆学校所使用的正方教务来获取课程表,其所使用的 RSA 加密方法没有现成的 Python 库可使用,目前其他的 CSDN 博客所采用的方法均无法成功登陆,故只能自己研究正方教务所采用的 crypto/rsa 的 JavaScript 实现并转义为 Python 语言实现。现开源为 PyRsa 库发布到 PyPI。
PyRsa 使用
安装
pip install pyrsa
基本介绍
本模块的基本思路为 1:1 等价转义原生的 JavaScript 库,正方教务上的加密库与安装后 PyRsa 库文件截图对比:
其中有几个移植 JavaScript 到 Python 过程中值得注意的问题:
- 无符号整数位移
由于 Python 独特的数据类型特性,在数据超过其数据类型长度后则会自动转化为 long,因此不存在JavaScript的 “>>>” 或者 “<<<” 的无符号整数操作,因此需要利用右移操作符 “>>” 来实现无符号整数右移:
def unsigned_right_shift(n, i):
"""
无符号整数右移
:param n:
:param i:
:return:
"""
def int_overflow(val):
maxint = 2147483647
if not -maxint - 1 <= val <= maxint:
val = (val + (maxint + 1)) % (2 * (maxint + 1)) - maxint - 1
return val
# 数字小于 0,则转为 32 位无符号 uint
if n < 0:
n = ctypes.c_uint32(n).value
# 正常位移位数是为正数,但是为了兼容 js 之类的,负数就右移变成左移
if i < 0:
return -int_overflow(n << abs(i))
# print(n)
return int_overflow(n >> i)
- 原生 js 库 jsbn 用来实现任意精度的整数算法,其 API 类似于java.math.BigIntegerJava 中的 类。在此库中,BigInteger 为一个 Array() 类,但考虑到 Python 的数据类型结构,PyRsa 库采用名为 int_dict 的字典来存储,其键值对与 Array() 的数据一一对应。
在下图中演示的是在 modulus 和 exponent 相同时 Python 实现的 BigInteger 类与 jsbn 库实现的 BigInteger 类的对应情况。其余参数对应 BigInteger 类的 proto 隐式原型。
- 利用 rng.js 库随机生成 seed 来填充字符串,使得每一次对相同原密码的加密后的字符串不可能重复产生。
基本使用
使用 RsaKey 模块
由于本模块是面向正方教务的加密,因此 modulus 长度应为 172;如果单纯是加密着玩的话,那么 modulus 与 pre_psw 两个参数的长度则有所限制,大概就是
modulus = int(psw / 3) + 15 + psw
嘛,谁会这么无聊干这事儿呢
from PyRsa.pyrsa import RsaKey
from PyRsa.pyb64 import Base64
modulus = "AJftLhHzsQPu1LwCgOR41hRKn4tbaD/ehyZKiBWDYCpaualtMyJIT0SzBl07O2NwjxI8uwr82SMvEW9iiSEoBylHOWNnEzyOYwXb29xMo+D4LTVqMX7NkAliIqH+wOSA1g0DVxmcQWCtGVI4vDUnGIN8tYPlxc9NIXN5zO0HwqKn"
exponent = 'AQAB'
rsakey.set_public(Base64().b64tohex(modulus), Base64().b64tohex(exponent))
psw = '1234567890'
en_psw = Base64().hex2b64(rsakey.encrypt(pre_psw))
模拟登陆正方教务
拿到经过 PyRsa 加密后的字符串后,就可以开始进行正方教务的模拟登陆了。新版正方教务基本样式如下:
完整实现思路
1. 黑盒模型
最终获取到的 JSESSIONID 相当于整个教务的唯一识别代码,可用其获取得到任意想获取到的数据。换句话说,JSESSIONID 包含了用户的模拟登陆信息,并有一定的生命周期(没具体测过,不超过一天)。
2. 主要函数实现:
-
获取初始 cookies:
通过抓包,在 login_slogin.html 页面的 Response Headers 找到 Set-Cookie 的两个参数 BIPipServerjwxtnew_BS80 和 JSESSIONID。
代码实现如下:def get_raw_cookies_csrf(self): headers = { 'Host': f'{self.domain}', 'Connection': 'keep-alive', 'Upgrade-Insecure-Requests': '1', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) ' 'Chrome/76.0.3809.87 Safari/537.36', 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;' 'q=0.8,application/signed-exchange;v=b3', 'Accept-Encoding': 'gzip, deflate', 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8' } url = f'http://{self.domain}/jwglxt/xtgl/login_slogin.html' res = self.session.get(url, headers=headers) doc = pq(res.text) self.csrf = doc('#csrftoken').attr('value') self.raw_cookie = requests.utils.dict_from_cookiejar(res.cookies)
-
获取 modulus 和 exponent 两个参数生成 RSA 公钥
利用上一步获取到的初始 cookies 的两个参数,在 xhr 异步网页 login_getPublicKey.html 用 GET 方法可以获取到对应 json 的两个参数 exponent 和 modulus。def get_json(self): self.getpublickey_t = int(time.time() * 1000) url = f'http://{self.domain}/jwglxt/xtgl/login_getPublicKey.html?time={self.getpublickey_t}' headers = { 'Accept': 'application/json, text/javascript, */*; q=0.01', 'Accept-Encoding': 'gzip, deflate', 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8', 'Connection': 'keep-alive', 'Cookie': f'JSESSIONID={self.raw_cookie["JSESSIONID"]};' f' BIGipServerjwxtnew_BS80={self.raw_cookie["BIGipServerjwxtnew_BS80"]}', 'Host': f