步骤一:识别加密

目的:找出请求里用于防爬或校验的“被加密”参数

找接口(步骤与操作)

  • 进入目标站点,在页面搜索框输入关键词并搜索

  • 打开开发者工具 —— Network,滚动页面或点击分页加载商品

  • 清空 Network,再触发一次加载(例如点击“第1页”)以便筛查

  • 使用 Network 的搜索(Ctrl + F),把一个商品标题粘贴进去,根据定位结果打开对应请求

  • 在请求的 Preview/Response 中查看 JSON,展开 data.resultList 可见商品条目

找参数(对比请求找动态字段)

  • 复制目标接口 URL,在 Network 的顶部筛选器里粘贴以仅显示该接口

  • 刷新页面或再次触发请求,比较两个或多个请求的 Payload(负载)

  • 记录所有参数并对比:出现变化的参数就是逆向重点

参数名

第一次请求值

第二次请求值

差异说明

t

1772445878640

1772446448187

时间戳变化:毫秒级时间戳,两次请求间隔约 (1772446448187 - 1772445878640) = 569546 毫秒(约570秒)

sign

d93bb646b...

c6c7f52437...

签名同步变化:签名由时间戳 t、接口参数等加密生成,时间戳改变则签名必然更新

其他参数

完全一致

完全一致

jsv/appKey/v/accountSite/api 等固定参数未变,说明是同环境、同接口的重复请求

步骤二:参数定位

目的:在网页加载的 JS 源码中找到哪个函数/代码片段生成该参数

找位置(全局搜索)

  • 使用正则精确匹配,例如 \bsign\b: 可避免匹配到 assigndesign

步骤三:参数解析

目的:理解加密函数的实现,明确它的输入与输出,并能复现在本地

找逻辑(查看变量与函数)

d = this.options
h = c.appKey || ("waptest" === d.subDomain ? "4272" : "12574478"), 
j = (new Date).getTime(), 
k = i(d.token + "&" + j + "&" + h + "&" + c.data), 
l = {
  jsv: A,
  appKey: h,
  t: j,
  sign: k
}
  • t(时间戳):直接取当前系统时间的毫秒级 Unix 时间戳(Date.now()),作为签名防重放的核心因子,服务端会校验时间偏差,超出阈值则拒绝请求。

  • sign(签名):通过固定格式拼接字符串后,调用前端已有i方法哈希生成i方法可直接复制函数实现,无需额外开发。核心拼接模板为 {token}&{t}&{appKey}&{data},将拼接好的字符串传i方法,即可生成 32/64 位十六进制签名串,参数顺序、格式(无多余空格 / 换行)必须完全匹配,否则签名失效。

  • token为前端会话密钥(token在Console控制台多次打印,看起来是恒定的,其实不然,而是大约一个半小时会发生变化,所以应该定位token生成)

  • appKey为固定业务标识、恒定可写死

  • data为 JSON 格式的业务参数,通常等于请求的表单数据(包含 pageNum、keyword 等)

  • t为时间戳

定位 token 生成

使用的 XHR 断点方法,定位 token 参数的生成位置(因为过程比较繁杂,这里就不具体展开了)

// 优先级判断
a.CDR && l(B) ? a.token = l(B).split(";")[0] : a.token = a.token || l(C)
// 格式清洗
a.token && (a.token = a.token.split("_")[0])
  • 核心 Cookie 键名:关键依赖两个预设 Cookie 键,核心为 _m_h5_tk,备选为 _m_h5_c

  • Cookie 读取优先级:优先判断是否开启 CDR 标识,若开启则读取 _m_h5_c;若未开启,优先复用已有 token,无已有值时则读取核心 Cookie _m_h5_tk

  • 格式清洗规则:无论读取哪个 Cookie,最终都需对值做统一处理 —— 按下划线 _ 分割字符串,取第一部分作为有效 token,剔除后缀冗余内容。

总结:token 的来源和 Cookie 紧密相关,时效由对应 Cookie 的有效期决定。

步骤四:参数调用

目的:将复现的签名逻辑集成到本地流程,用生成的加密参数发起合法请求并获取数据

把加密函数及其调用逻辑封装成一个可调用函数(JS 文件内)

//goofish_sign_crack.js

function get_sign(data,token,h) {
  function i(a) {...}
  j = (new Date).getTime()  
  k = i(token + "&" + j + "&" + h + "&" + data)
  return [j,k]
}

在 Python 中用 execjs 调用该函数得到加密参数的值

第一步:准备js代码需要的所有参数

token = cookies.get('_m_h5_tk', '').split('_')[0]
appKey = params['appKey']

第二步:读取 JS 代码文件

建议使用utf-8编码`打开文件,避免中文或特殊字符导致解析错误。

with open('goofish_sign_crack.js', 'r', encoding='utf-8') as f:
    js_code = f.read()

第三步:编译 JS 代码为可调用的上下文

使用 execjs.compile(js_code) 创建一个隔离的 JS 环境。

import execjs
js = execjs.compile(js_code)

第四步:调用 JS 函数获取签名

使用 js.call('get_sign', 参数1, 参数2, ...) 调用并获取返回值。

q = js.call('get_sign',data['data'],token,appKey)
# q 的形式为 [t, sign]
params['t'] = q[0]
params['sign'] = q[1]

print("生成的有效 t 和 sign:", q)

完整的.py代码如下:

# 导入execjs模块,用于执行JavaScript代码(生成签名)
import execjs
# 导入requests模块,用于发送HTTP请求
import requests

# 定义请求所需的Cookie参数(从浏览器抓包获取,包含闲鱼会话信息、token等关键数据)
cookies = {
    't': 'ca87ebc39b662ec0e1e8cadddf55ab37',  
    'cna': 'm+P9IarevSIBASQJilyUngif',        
    'isg': 'BJGRzWjsnD0rBfAp18pOEhZwoJ0r_gVwxQjuHHMnJNh3Grlsu07NQO95uO78Jp2o',  
    'cookie2': '1dde233c05f155d92918aa02f401f822',  
    'xlly_s': '1',                             
    '_samesite_flag_': 'true',                 
    '_tb_token_': '3eb16187b0db3',             
    'mtop_partitioned_detect': '1',            
    '_m_h5_tk': 'bdca7f16aac67b1af76b36eac6d929b3_1772469031135',  
    '_m_h5_tk_enc': '4bce1c2deda447408314028f5c547899',  
    'tfstk': 'gdYxnXq92YDcPagSprmosHQmAWluD0A2mKRQSOX0fLpJOBycfEbDBlpD_djs3ZY9eC9Kg1D2SS-yKBaDom7gWZ7N5vDn-2ADgN71X06-C-W5i60gcGafuaZBnI3K-2A2GSfjtQgHmyFGe1W15is_V_6P6o16CGaWNT5N1-1_lbd5UT1bCta1Fu1Cs1Z6CNGJNT5ahNsXCgGR_T615NMm3SBgGOU92R5sy7q0EN7pysIAdQnUWPwPEJXJGuzT5UpxL9dbbPa6ysIvyaqXVnIDfBSeoEgTzZApVa1HgqURRI1pnTvIRzQWZQ9GY3ka-gJJWGTf05G5FEpJuZ9mfypdkdIBlgF_SBd5HaCWRfqFEUBDCEIStyIGPejCl3mrnGbA9dT2H5U6dQAHug8tp86wm675tIuYywOf1g-w-e3pwJXdsoG-wlrNc_-eVSvgzJuzl_BnZuqabgfPw9c-wlrNc_5RKb0ublSla',  
}

# 定义请求头(模拟浏览器请求,避免被风控拦截)
headers = {
    'accept': 'application/json',  
    'accept-language': 'zh-CN,zh;q=0.9',  
    'content-type': 'application/x-www-form-urlencoded',  
    'origin': 'https://www.goofish.com',  
    'priority': 'u=1, i',  
    'referer': 'https://www.goofish.com/',  
    'sec-ch-ua': '"Not:A-Brand";v="99", "Microsoft Edge";v="145", "Chromium";v="145"',  
    'sec-ch-ua-mobile': '?0',  
    'sec-ch-ua-platform': '"Windows"',  
    'sec-fetch-dest': 'empty',  
    'sec-fetch-mode': 'cors',   
    'sec-fetch-site': 'same-site',  
    'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36 Edg/145.0.0.0',  
   
}

# 定义MTOP接口URL参数(接口核心配置)
params = {
    'jsv': '2.7.2',  
    'appKey': '34839810',  
    't': '1772460078712',  
    'sign': 'd7bb1ed82e5a541415673979380434d0',  
    'v': '1.0',  
    'type': 'originaljson',  
    'accountSite': 'xianyu',  
    'dataType': 'json',  
    'timeout': '20000',  
    'api': 'mtop.taobao.idlemtopsearch.pc.search',  
    'sessionOption': 'AutoLoginOnly',  
    'spm_cnt': 'a21ybx.search.0.0',  
    'spm_pre': 'a21ybx.home.searchHistory.1.4c053da6OdvVjl',  
    'log_id': '4c053da6OdvVjl',  
}

# 定义请求体数据(搜索业务参数)
data = {
    # JSON字符串:搜索关键词、分页、排序等核心参数
    'data': '{"pageNumber":2,"keyword":"小米17promax","fromFilter":false,"rowsPerPage":30,"sortValue":"","sortField":"","customDistance":"","gps":"","propValueStr":{},"customGps":"","searchReqFromPage":"pcSearch","extraFilterValue":"{}","userPositionJson":"{}"}',
}

# 核心步骤1:提取_m_h5_tk中的有效token(按_分割取第一段,签名关键)
token = cookies.get('_m_h5_tk', '').split('_')[0]
# 提取appKey(从params中获取,避免硬编码)
appKey = params['appKey']

# 核心步骤2:读取JS签名脚本(包含sign生成逻辑)
with open('goofish_sign_crack.js', encoding='utf-8') as f:
    js_code = f.read()

# 编译JS代码(准备执行)
js = execjs.compile(js_code)

# 核心步骤3:调用JS中的get_sign函数生成t(时间戳)和sign(签名)
# 入参:业务data、token、appKey;返回值:[t, sign]
q = js.call('get_sign', data['data'],token,appKey)

# 更新params中的t和sign为动态生成的值(替换初始值)
params['t'] = q[0]
params['sign'] = q[1]
# 打印生成的t和sign,便于调试
print("生成的有效 t 和 sign:", q)

# 核心步骤4:发送POST请求调用闲鱼MTOP搜索接口
response = requests.post(
    'https://h5api.m.goofish.com/h5/mtop.taobao.idlemtopsearch.pc.search/1.0/',  # 接口地址
    params=params,       # URL参数
    cookies=cookies,     # Cookie参数
    headers=headers,     # 请求头
    data=data,           # 请求体
)

# 打印响应结果(调试用)
print("响应状态码:", response.status_code)  # 打印状态码(200为成功)
print("响应文本:", response.text)          # 打印原始响应文本(避免JSON解析失败)
try:
    # 尝试解析JSON格式响应(便于结构化查看)
    print("响应 JSON:", response.json())
except Exception as e:
    # JSON解析失败时打印错误信息
    print("JSON 解析失败:", e)