别着急,坐和放宽
标签:Python 爬虫、逆向分析、RSA 加密、Requests、BeautifulSoup
环境:Windows 10 x64 + Python 3.11 + requests 2.32 + beautifulsoup4 4.12
示例站点:文中涉及的域名、Cookie 名均已脱敏,替换为example.edu.cn
/learning.example.edu.cn
等占位符,仅供技术研究。
在很多高校或企业的在线教学 / 教务平台中,都会使用 CAS 单点登录(Central Authentication Service)。 当我们想定时抓取课表、下载资料或做自动签到时,反复手动输入学号密码极不方便,于是需要一个“无头”脚本自动完成登录。
子任务 | 说明 | 成功判据 |
---|---|---|
① 拿到 execution | CAS 登录页的动态隐藏字段 | 打印值形如 e1s1 … |
② 获取公钥 | /cas/v2/getPubKey 返回 modulus 、exponent | 能解析为大整数 |
③ 复刻前端加密 | security.js → encryptedString() | 与浏览器输出一致 |
④ 提交表单 | 含账号、密文密码、execution | 服务器 302,Location 携带 ticket= |
⑤ 跟随票据 | .../fromcas?ticket=ST-... | 服务器 Set‑Cookie 下发 AUTHORIZATION= |
⑥ 打印 Cookie | sess.cookies.get("AUTHORIZATION") | 终端输出 32 字节十六进制串 |
https://auth.example.edu.cn/cas/login?service=https://learning.example.edu.cn/api/fromcas
。ticket=ST-...
。浏览器自动 GET 该 ticket 地址,服务端再次 302 到首页,同时发放业务域 Cookie:
Set-Cookie: AUTHORIZATION=3EE2248EABJH8CBB920881E67729341A; Domain=.example.edu.cn; Path=/
execution
每次刷新都会变;旧值会提示“页面闲置时间过长”。在登录页源码中搜索 execution
即可发现类似:
<input type="hidden" name="execution" value="e3s1" />
在脚本里,用正则或 BeautifulSoup 提取即可:
security.js
—— 反向 RSA 加密前端的核心逻辑(已简化)如下:
encryptedString()
把两个字节拼成一 digit,低字节在前。成功标志:resp.status_code == 302
且 Location
含 ticket=
。
完整脚本已脱敏示例:
症状 | 原因 | 解决 |
---|---|---|
提示“页面闲置时间过长” | execution 过期 | 每次先 GET 登录页,立即提交 |
返回 200 而非 302 | 密码加密不符 | 再对比浏览器密文,注意反转 & Little‑Endian |
AUTHORIZATION=None | 未访问 ticket URL / ticket 失效 | 跟随重定向或重新登录 |
验证码出现 | 失败次数过多被风控 | 人工输入或接入打码平台 |
免责声明
本文所有技术仅供个人学习与研究,示例域名与 Cookie 均为虚构,不对应任何真实系统。请勿将脚本用于未经授权的批量访问,否则后果自负。
html = sess.get(LOGIN_PAGE).text
execution = re.search(r'name="execution" value="(.*?)"', html).group(1)
var reversed = password.split('').reverse().join('');
var key = RSAUtils.getKeyPair(exp, '', mod);
var cipher = RSAUtils.encryptedString(key, reversed);
import math
def js_style_rsa(pwd, mod_hex, exp_hex):
rev = pwd[::-1].encode()
n, e = int(mod_hex, 16), int(exp_hex, 16)
chunk = 2 * (math.ceil(n.bit_length()/16) - 1)
buf = bytearray(rev)
while len(buf) % chunk:
buf.append(0)
return ' '.join(
format(pow(int.from_bytes(buf[i:i+chunk], 'little'), e, n), 'x')
for i in range(0, len(buf), chunk)
)
form = {
'username': user,
'password': js_style_rsa(pwd, pub['modulus'], pub['exponent']),
'authcode': '',
'execution': execution,
'_eventId': 'submit'
}
resp = sess.post(LOGIN_PAGE, data=form, allow_redirects=False)
redirect_url = resp.headers['Location']
sess.get(redirect_url, allow_redirects=True)
auth = sess.cookies.get('AUTHORIZATION')
print('AUTHORIZATION =', auth)
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""cas_login.py —— 通用 CAS 登录脚本示例(域名已脱敏)"""
import requests, re, math
from getpass import getpass
LOGIN_PAGE = (
'https://auth.example.edu.cn/cas/login'
'?service=https://learning.example.edu.cn/api/fromcas'
)
PUBKEY_API = 'https://auth.example.edu.cn/cas/v2/getPubKey'
def js_style_rsa(pwd, mod, exp):
rev = pwd[::-1].encode()
n, e = int(mod, 16), int(exp, 16)
chunk = 2 * (math.ceil(n.bit_length()/16) - 1)
buf = bytearray(rev)
while len(buf) % chunk:
buf.append(0)
return ' '.join(format(pow(int.from_bytes(buf[i:i+chunk], 'little'), e, n), 'x')
for i in range(0, len(buf), chunk))
def main():
user = input('学号 / 用户名: ').strip()
pwd = getpass('密码(输入时不回显): ')
s = requests.Session()
s.headers['User-Agent'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'
html = s.get(LOGIN_PAGE).text
execution = re.search(r'name="execution" value="(.*?)"', html).group(1)
pub = s.get(PUBKEY_API).json()
enc = js_style_rsa(pwd, pub['modulus'], pub['exponent'])
data = {'username': user, 'password': enc, 'authcode': '',
'execution': execution, '_eventId': 'submit'}
r = s.post(LOGIN_PAGE, data=data, allow_redirects=False)
if 'Location' not in r.headers:
print('登录失败')
return
s.get(r.headers['Location'], allow_redirects=True)
print('✅ AUTHORIZATION =', s.cookies.get('AUTHORIZATION'))
if __name__ == '__main__':
main()