希赛网登录接口逆向分析
希赛网登录接口逆向分析
一、抓包分析登录请求

打开浏览器开发者工具(F12),切换到 Network 面板并保持录制,然后在登录页输入任意账号密码并点击登录,观察新发起的请求。
先看请求载荷(Payload):loginName 和 password 均为明文传输;imgCode 从命名即可判断是图形验证码;imgKey 暂时还不清楚用途,留到后面分析;sk 是固定值,这里为 loginName。
再看一下这次请求的响应结果:
{"extraDatas":{},"resultCode":"ILLEGAL_PARAM","resultMsg":"账号不存在"} // 因为账号密码是我随便填的,所以返回"账号不存在"

二、分析请求头中的加密参数
切换到 Headers 面板,多次用不同账号密码触发登录请求后可以发现:请求头中的 sscc 和 sscc2 是加密参数,每次请求都会变化;而 sid 保持不变。

顺带看一下 Cookie:
mddcid=1789345294577b4YasF; anyid=9676126f59c045ff9f01f54dde7eab98166886; anyid=9676126f59c045ff9f01f54dde7eab98166886; Hm_lvt_555d9dcffdcb317595de82b0fc125cdf=1789399046; 53gid2=15895856115001; 53revisit=1789399045703; _er_=top-login; mddsync=1; enterPage=https%3A%2F%2Fwangxiao.xisaiwang.com%2Fucenter2%2Flogin.html; enterPageReferrer=https%3A%2F%2Fwangxiao.xisaiwang.com%2F; referrer=https%3A%2F%2Fwangxiao.xisaiwang.com%2F; _sid_=cde38729449a18dff58d69b320b18076; _sid_=b7ee17405764c392705876a482fbedf6; mddldyurl=https%3A%2F%2Fwangxiao.xisaiwang.com%2Fucenter2%2Flogin.html; fromUrl=https%3A%2F%2Fwangxiao.xisaiwang.com%2F; Hm_lvt_4b9f46d3a269fb3925580a1827143bfb=1789393295,1789473929,1789540140,1789560253; HMACCOUNT=0F0A3D5081CF13D2; Hm_lpvt_4b9f46d3a269fb3925580a1827143bfb=1789573402
Cookie 里有两个重复的 anyid,同时 _sid_ 的值和请求头中的 sid 参数一致。后续用 Python 构造请求时,记得去掉重复的 anyid,并把 _sid_ 作为关键参数保留下来。
三、全局搜索加密参数,定位加密函数
在 Sources 面板中使用全局搜索(快捷键 Ctrl+Shift+F),同时搜索 sscc 和 sscc2:

搜索结果中第一个命中的 getSscc 看起来就是生成 sscc 的函数,点进去看一下:

函数末尾 return sscc,可以确定它就是 sscc 的生成函数。
四、断点调试 getSscc 函数
在函数入口处打上断点,回到页面点击登录,让请求命中断点:

原函数getSscc: function (key) {
var _sid_ = "";
var arr = document.cookie.match(new RegExp("(^| )_sid_=([^;]*)(;|$)"));
if (arr != null) try {_sid_ = decodeURIComponent(arr[2]);}catch(error){console.log("getCookie error")}
var sscc = hex_md5(_sid_ + key);
return sscc;
}
可以看到:传入的参数 key 就是登录账号;而 sscc 的生成逻辑就是把 _sid_ 和账号直接拼接后做一次 MD5。
同时根据函数中var _sid_ = "";
var arr = document.cookie.match(new RegExp("(^| )_sid_=([^;]*)(;|$)"));
函数里读取 _sid_ 的逻辑也很清楚:通过正则从 document.cookie 中匹配出 _sid_ 字段。因此在整个会话期间 _sid_ 保持不变,在 Python 脚本里可以先写死,等过期了再手动替换一次。
接下来验证一下 hex_md5() 是不是标准的 MD5(当然也可以直接读函数体确认,这里为了省步骤,直接用真实数据测):
取一组实际数据:_sid_ = "cde38729449a18dff58d69b320b18076",key = "12345678"。
抓包得到的 sscc = "cedfc5f502cbee6373c61c723b9b68a4"。

把 _sid_ 与 key 拼接后做一次 MD5,得到的结果和抓包里的 sscc 完全一致,验证通过。
至此 sscc 的生成逻辑已经完全搞清楚,接下来分析 sscc2。
五、定位 sscc2 的赋值位置
再次全局搜索 sscc2:

可以看到函数beforeSend: function (xhr) {
//发送ajax请求之前向http的head里面加入验证信息
xhr.setRequestHeader("clientType", Kino.getport());
console.log(configs.sk);
if (configs.sk) {
if (typeof (Kino.flushCookie) == "function") {
Kino.flushCookie();
}
xhr.setRequestHeader("sscc", Kino.getSscc(configs.sk));
xhr.setRequestHeader("sscc2", Kino.getAsciiKey(this.data));
var _sid_ = "";
var arr = document.cookie.match(new RegExp("(^| )_sid_=([^;]*)(;|$)"));
if (arr != null) _sid_ = decodeURIComponent(arr[2]);
if (_sid_ && _sid_ != "") {
xhr.setRequestHeader("sid", _sid_);
}
}
}
从 xhr.setRequestHeader("sscc2", Kino.getAsciiKey(this.data)) 这一行可以看出:请求头中的 sscc2 是由 Kino.getAsciiKey(this.data) 赋值的,而 this.data 就是本次登录请求的完整表单字符串。
直接全局搜索定位 getAsciiKey 方法,打上断点并再次触发登录请求。
六、断点调试 getAsciiKey 函数

原函数是getAsciiKey: function (qureyStr) {
var obj = {}, pairs = qureyStr.split('&'), d = unescape, name, value;
$.each(pairs, function (i, pair) {
pair = pair.split('=');
name = d(pair[0]);
value = d(pair[1]);
obj[name] = value;
});
var asciiStr = this.sortAscii(obj);
var len = asciiStr.length;
var init = 0;
var key = "";
while(init < len) {
key += asciiStr.charAt(init);
init += 3;
}
return key;
}
命中断点时,传入函数的参数正是完整的表单字符串:loginName=12345678&password=123456&imgCode=d89c&imgKey=29158252&sk=loginName。
逐行分析这个函数:它先把传入的字符串按 & 切分成数组:

再把每一段按 = 切分,把键值对塞进 obj 对象:

var asciiStr = this.sortAscii(obj) 这一行是关键:它把 obj 的所有键按 ASCII 码从小到大排序,再用固定规则重新拼成一个字符串。
这里得到的 asciiStr 为:
asciiStr="imgCode=d89c&imgKey=29158252&loginName=12345678&password=123456&sk=loginName"
和原始入参相比,格式完全一致,只是键的顺序变了(按 ASCII 排序)。为了严谨,我们再进 this.sortAscii() 内部看一眼:
sortAscii: function (obj) {
var arr = new Array();
var num = 0;
for (var i in obj) {
arr[num] = i;
num++;
}
var sortArr = arr.sort();
var str = '';
for (var i in sortArr) {
str += sortArr[i] + '=' + obj[sortArr[i]] + "&"
}
var char = '&';
str = str.replace(new RegExp('^\\' + char + '+|\\' + char + '+$', 'g'), '');
return str;
}
其功能和上面分析的完全一致:把对象的键取出来排序,再以 key=value& 的形式拼接,最后用正则去掉首尾多余的 &。
回到 getAsciiKey,继续看 asciiStr 之后的处理:
var init = 0;
var key = "";
while(init < len) {
key += asciiStr.charAt(init);
init += 3;
}
return key;
可以看出:它从排好序的 asciiStr 的第 0 位开始,每隔 3 个字符取一个(步长为 3),把取到的字符拼接成最终的 key。这里得到的结果是 "iCe8&gy982onm147psr14&=gNe",它就是请求头里的 sscc2。
回到抓包的请求头里核对一下:

抓包中的 sscc2 和我们算出来的结果完全一致,分析无误。
七、回顾 imgKey 的生成逻辑
前面载荷里的 imgKey 还没解释,现在回头看一下:

可以看到 imgKey 是由一个叫 uuid 的函数生成的。但它生成的值看起来并不像标准的 UUID v1 或 UUID v4。
那我们也去全局搜一下 uuid:

给 uuid 打断点并刷新页面触发,可以看到生成长度 len = 8;再对比多次抓到的 imgKey 值,发现这 8 位全是数字。
也就是说,imgKey 可以直接本地构造:从 0~9 中随机取 8 个数字拼接即可。
接下来要解决一个新问题:
载荷中的 imgCode 是图形验证码,必须先把验证码图片识别出来才能登录。
下一步就去定位验证码图片的生成逻辑。
验证码图片的 URL 规律
在页面上找到验证码 img 元素,它的 src 就是验证码图片的链接:
https://base.xisaiwang.cn/verify/code/r/image.do?key=68037516

直接访问这个链接验证一下:

确实返回了一张验证码图片,验证通过。
但在 Python 脚本里我们需要自己构造这个验证码链接,所以得摸清楚它的规律。
对比多次刷新验证码时的链接:
当前验证码图片链接:
https://base.xisaiwang.cn/verify/code/r/image.do?key=68037516
点击"换一张"或刷新页面后的链接:
https://base.xisaiwang.cn/verify/code/r/image.do?key=68449766
可以看到只有 key 在变。再把它和登录请求载荷里的 imgKey 对比一下:
会发现:登录请求载荷里的 imgKey,和验证码图片 URL 里的 key 完全一致。
也就是说,验证码图片 URL 中的 key 就是我们前面本地构造的那个 8 位随机数字 imgKey,直接拼到 URL 里就能拉到对应的验证码图片。
到这里,登录接口的所有加密参数就全部分析完毕。接下来就可以开始写代码了——验证码图片通过请求上面的链接拿到后,用 ddddocr 本地识别即可(也可以换成打码平台或其他识别库)。
八、Python 代码实现
下面是一个最小可运行的 Demo(仅用于验证逆向思路,正式项目里需要补充异常处理、会话管理和参数过期自动刷新等):
import requests
import random
import ddddocr
import hashlib
session = requests.session()
# 自己生成 imgKey
img_key = ''.join(random.choices('0123456789', k=8))
loginName = input('请输入账号:')
pwd = input("请输入密码:")
# 请求验证码图片
captcha_url = f'https://base.xisaiwang.cn/verify/code/r/image.do?key={img_key}'
captcha_resp = session.get(captcha_url)
ocr = ddddocr.DdddOcr()
# 识别
img_code = ocr.classification(captcha_resp.content)
data = {
'loginName': loginName,
'password': pwd,
'imgKey': img_key,
'imgCode': img_code,
'sk': 'loginName',
}
data_li = []
for i in data:
data_li.append(i)
data_li.sort()
str = ''
for i in data_li:
if i != data_li[-1]:
str += f'{i}={data[i]}&'
else:
str += f'{i}={data[i]}'
key = str[0::3]
cookies = {
'mddcid': '1789345294577b4YasF',
'anyid': '9676126f59c045ff9f01f54dde7eab98166886',
'Hm_lvt_555d9dcffdcb317595de82b0fc125cdf': '1789399046',
'53gid2': '15895856115001',
'53revisit': '1789399045703',
'_er_': 'top-login',
'enterPage': 'https%3A%2F%2Fwangxiao.xisaiwang.com%2Fucenter2%2Flogin.html',
'enterPageReferrer': 'https%3A%2F%2Fwangxiao.xisaiwang.com%2F',
'referrer': 'https%3A%2F%2Fwangxiao.xisaiwang.com%2F',
'_sid_': 'cde38729449a18dff58d69b320b18076',
'mddsync': '1',
'mddldyurl': 'https%3A%2F%2Fwangxiao.xisaiwang.com%2Fucenter2%2Flogin.html',
'fromUrl': 'https%3A%2F%2Fwangxiao.xisaiwang.com%2F',
'Hm_lvt_4b9f46d3a269fb3925580a1827143bfb': '1789345295,1789393295,1789473929,1789540140',
'Hm_lpvt_4b9f46d3a269fb3925580a1827143bfb': '1789560992',
'HMACCOUNT': '0F0A3D5081CF13D2',
}
# 简单 MD5 加密
text = f"{cookies['_sid_']}{loginName}"
sscc = hashlib.md5(text.encode("utf-8")).hexdigest()
headers = {
'Accept': 'application/json, text/javascript, */*; q=0.01',
'Accept-Language': 'zh-CN,zh;q=0.9',
'Connection': 'keep-alive',
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
'Origin': 'https://wangxiao.xisaiwang.com',
'Referer': 'https://wangxiao.xisaiwang.com/ucenter2/login.html',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'same-origin',
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.0.0 Safari/537.36',
'X-Requested-With': 'XMLHttpRequest',
'clientType': 'PC',
'sec-ch-ua': '"Chromium";v="152", "Not?A_Brand";v="24", "Google Chrome";v="152"',
'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': '"Linux"',
'sid': 'cde38729449a18dff58d69b320b18076',
'sscc': sscc, # sid加账号
'sscc2': key,
}
session.cookies.update(cookies)
session.headers.update(headers)
# 提交登录
login_url = 'https://wangxiao.xisaiwang.com/ucenter2/user/doLogin.do'
resp = session.post(login_url, data=data)
print("登录响应:", resp.json())
注意事项:由于博客排版格式限制,有部分代码缩进需要使用者自行更正。
运行一下,看看结果:

请求成功发出,响应结果符合预期。需要注意:_sid_ 是 Cookie 中具有时效性的会话标识,如果代码跑出来的结果不对,先去浏览器里刷新一下页面,把最新的 _sid_ 替换到脚本里再试。
至此,整个希赛网登录接口的逆向分析就全部完成了。
使用提示:本文仅用于学习研究前端加密与爬虫原理,实际使用请遵守目标站点服务条款与 robots 协议,控制请求频率,不要对站点造成压力。
再次声明:本文所有内容仅面向合法合规的技术交流与学习研究,不允许任何主体将本文内容用于商业项目、非法爬虫活动等任何未经授权的场景。否则,一切责任由使用者自行承担。