# -*- coding: utf-8 -*- # Author : Charley # Python : 3.12.10 # Date : 2026/07/02 """卡集 App Open-Auth-Sig 请求签名模块。 卡集(card.kaji.com,uni-app / DCloud)的鉴权头 Authorization 采用 Open-Auth-Sig 方案:每个请求用 [随机 swap + 当前时间戳 + 随机 nonce + 接口路径] 拼串做标准 MD5, 再经一张 256 项固定置换表编码成 Signature。全程不含任何密钥 / appSecret / 登录 token, 纯客户端即可复现,因此爬虫可长期无人值守运行,不存在凭证过期问题。 算法来源:app-service.js(APK 反编译产物)模块 "665c" 的 GetCrypto 函数, 已用真实抓包样本逐字节验证通过。videoPlay 的 body 签名来自偏移 1203166 处的 video_sign。 """ import time import random import hashlib # 256 项置换表:来自 app-service.js 偏移约 1513290 处的常量数组 i,是 0..255 的一个全排列。 # gen_signature 会先按 swap 的 4 对字节对它做两两交换,再逐字符查表。 I_TABLE = [ 194, 115, 46, 172, 195, 57, 45, 12, 78, 181, 246, 168, 16, 218, 68, 248, 242, 184, 7, 190, 216, 59, 160, 189, 14, 117, 113, 72, 210, 0, 120, 69, 15, 48, 141, 128, 34, 155, 19, 38, 110, 63, 76, 240, 35, 49, 56, 47, 229, 196, 118, 244, 9, 174, 238, 252, 132, 143, 219, 21, 165, 22, 158, 119, 147, 85, 29, 233, 236, 146, 88, 27, 30, 5, 123, 221, 255, 83, 24, 247, 70, 111, 11, 4, 31, 159, 103, 6, 185, 52, 98, 75, 95, 142, 50, 3, 2, 203, 178, 109, 193, 64, 108, 179, 62, 94, 133, 180, 192, 17, 105, 214, 245, 28, 226, 87, 227, 215, 102, 201, 107, 202, 149, 124, 13, 208, 138, 188, 1, 54, 127, 139, 239, 209, 162, 243, 126, 144, 204, 100, 130, 187, 131, 207, 211, 217, 43, 80, 122, 112, 151, 154, 156, 161, 36, 205, 39, 140, 74, 25, 37, 164, 177, 33, 41, 61, 66, 234, 92, 44, 96, 79, 90, 99, 166, 153, 182, 150, 20, 106, 77, 93, 157, 135, 152, 104, 223, 173, 81, 134, 175, 230, 199, 250, 82, 10, 235, 222, 200, 114, 89, 148, 237, 65, 167, 125, 42, 198, 60, 129, 101, 228, 170, 86, 249, 55, 8, 97, 176, 73, 251, 213, 26, 136, 254, 169, 145, 225, 186, 32, 212, 197, 163, 241, 84, 67, 91, 253, 51, 224, 40, 191, 231, 53, 183, 58, 220, 116, 206, 232, 18, 71, 171, 23, 121, 137 ] # videoPlay body 签名的固定盐,来自 app-service.js 偏移 1203166: # sign = md5(f"{ts}_{goodCode}_{playCode}_videoPlayKsj") VIDEO_SIGN_SALT = "videoPlayKsj" # 接口统一前缀,对应 app-service.js 里 o.app.requestVersion API_PREFIX = "/api/v4/" def _md5_hex(text: str) -> str: """计算字符串的标准 MD5 十六进制摘要。 Args: text (str): 待哈希的明文串。 Returns: str: 32 位小写十六进制 MD5 值。 """ return hashlib.md5(text.encode("utf-8")).hexdigest() def gen_signature(f_hex: str, c: list[int]) -> str: """将 MD5 十六进制串经置换表编码为 128 位 Signature。 对应 app-service.js 模块 665c 的编码逻辑:先把 32 字符的 md5-hex 当作 ASCII 字符串再转 hex(得到 64 字符),复制置换表并按 swap 的 4 对字节做两两交换, 最后对每个字符以其 ASCII 码作下标查表,输出 2 位大写 hex 拼接。 Args: f_hex (str): 拼接串的 MD5 值(32 位小写 hex)。 c (list[int]): swap 对应的 8 个字节(0-255),决定置换表的 4 对交换。 Returns: str: 128 位大写十六进制的 Signature。 """ r = f_hex.encode("ascii").hex() # md5-hex 当 ASCII 再转 hex -> 64 字符 a = list(I_TABLE) # 复制置换表,避免污染全局常量 for s in range(0, len(c), 2): # 按 swap 的 4 对字节两两交换 a[c[s]], a[c[s + 1]] = a[c[s + 1]], a[c[s]] out = [format(a[ord(ch)], "02X") for ch in r] # 逐字符以 ASCII 码查表 return "".join(out) def gen_authorization(path: str) -> str: """为指定接口路径生成完整的 Open-Auth-Sig 请求头值。 每次调用都用当前时间戳与随机 swap / nonce 现算,天然免过期、免登录 token。 Args: path (str): 接口相对路径,不含域名与 query,如 "goodlist/forsale/main"。 函数内部自动补 API_PREFIX 前缀、去掉 "/dataApi" 子串与 query。 Returns: str: 形如 'Open-Auth-Sig Swap="..",Timestamp=..,Nonce=..,Signature=".."'。 """ ts = int(time.time()) # 当前 Unix 秒 noce = random.randint(1, 500) # 随机 nonce,闭区间 [1,500] full_path = (API_PREFIX + path).replace("/dataApi", "").split("?")[0] c = random.sample(range(256), 8) # 8 个不重复字节 swap = "".join(f"{b:02x}" for b in c) # 小写 hex,16 字符 body = f"{swap}_{ts}_{noce}_{full_path}" # 拼接顺序:swap_ts_noce_path sig = gen_signature(_md5_hex(body), c) return f'Open-Auth-Sig Swap="{swap}",Timestamp={ts},Nonce={noce},Signature="{sig}"' def video_sign(ts: int, good_code: str, play_code: str) -> str: """计算 good/videoPlay 接口 body 里的 sign。 对应 app-service.js 偏移 1203166:sign = md5(f"{ts}_{goodCode}_{playCode}_videoPlayKsj")。 Args: ts (int): Unix 秒时间戳,与 body 里的 ts 字段保持一致。 good_code (str): 商品编码,取自请求 URL good/videoPlay/{goodCode}。 play_code (str): 播放码,来自商品详情接口。 Returns: str: 32 位小写十六进制的 sign。 """ return _md5_hex(f"{ts}_{good_code}_{play_code}_{VIDEO_SIGN_SALT}")