Python爬虫中如何生成网站需要的签名(token)

我用 python 爬取一个海外电商网站数据。登录都搞定了,得到了 remember_key。但接下来,获取 order 的 detail 数据时,要求 POST 提交的 form 数据中,有一个 token。不知道是怎么生成的,token 是变长的,40 个字符左右变动。
请问如何破?我怀疑是本地 js 来生成的,但看了 js,并没有这样的操作啊。
Python爬虫中如何生成网站需要的签名(token)

14 回复

那个不是用过防 csrf 的嘛.


在Python爬虫里生成网站签名(token),核心就是逆向分析目标网站的JavaScript代码,找到它的签名算法,然后用Python复现。

最常见的情况是,网站会用JavaScript对请求参数(比如时间戳、特定数据)进行某种加密(如MD5, SHA, HMAC, AES)或编码(Base64),生成一个signtoken参数。你的任务就是找到并模拟这个过程。

基本步骤和代码示例:

  1. 定位签名代码:在浏览器开发者工具的“Network”面板中,找到携带tokensign的请求。查看其“Initiator”或直接搜索关键函数名(如 signencryptmd5hmac)。
  2. 分析算法:找到生成签名的JS函数,理解它的输入(哪些参数)、处理逻辑(拼接顺序、加密方式)和输出。
  3. Python复现:用Python的加密库(hashlib, hmac, base64)和标准库(json, time, random)重新实现这个逻辑。

一个典型的HMAC-SHA256签名生成示例:

假设分析发现,网站签名是将 时间戳 + 请求方法 + 请求路径 + 排序后的参数字符串HMAC-SHA256 加密,并以16进制输出。

import hashlib
import hmac
import time
import urllib.parse

def generate_sign(api_secret, method, path, params):
    """
    生成HMAC-SHA256签名。
    :param api_secret: 你的密钥(从网站获取或逆向得到)
    :param method: 请求方法,如 'GET', 'POST'
    :param path: 请求路径,如 '/api/v1/order'
    :param params: 请求参数字典
    :return: 签名字符串
    """
    # 1. 对参数字典按key进行排序并编码为查询字符串
    sorted_params = sorted(params.items(), key=lambda x: x[0])
    query_string = urllib.parse.urlencode(sorted_params)

    # 2. 拼接待签名字符串 (常见格式,具体需按网站调整)
    # 例如: 时间戳 + 方法 + 路径 + 查询字符串
    timestamp = str(int(time.time() * 1000))  # 毫秒时间戳
    string_to_sign = f"{timestamp}{method}{path}{query_string}"

    # 3. 使用HMAC-SHA256计算签名
    digest = hmac.new(
        key=api_secret.encode('utf-8'),
        msg=string_to_sign.encode('utf-8'),
        digestmod=hashlib.sha256
    ).hexdigest()  # 输出16进制字符串

    # 通常签名会和时间戳一起返回
    return timestamp, digest

# 使用示例
my_secret = "your_api_secret_here"
req_method = "GET"
req_path = "/api/v1/data"
req_params = {"symbol": "BTCUSDT", "limit": 100}

ts, sign = generate_sign(my_secret, req_method, req_path, req_params)
print(f"Timestamp: {ts}")
print(f"Signature: {sign}")

# 然后将 ts 和 sign 作为请求头或参数附加到你的请求中
headers = {
    "X-Timestamp": ts,
    "X-Signature": sign
}
# ... 使用requests库发送请求

关键点:

  • 密钥api_secret 通常是关键,可能需要登录后获取或从JS代码中固定值提取。
  • 拼接顺序string_to_sign 的格式必须和网站完全一致,多一个空格或少一个符号都会导致签名无效。
  • 编码:确保所有字符串在加密前都转换为正确的字节格式(.encode('utf-8'))。
  • 复杂情况:有些网站会使用自定义的加密函数或混淆过的JS。这时可能需要用 execjsPyExecJS 库直接执行JavaScript代码片段,或者用 Selenium / Playwright 这类浏览器自动化工具来获取动态生成的token。

总结:核心就是逆向JS,然后用Python的加密库1:1还原算法。

token 是登录的时候后端返回的吧

大概率 js 或者之前某个请求返回值

看一下是不是 CSRF_TOKEN

。并不是。登录时,后端返回的是 remember_key=xxxxx。没有 token

我把请求包发一下。
:authority: sell.souq.com
:method: POST
:path: /orders/getUnitListDetails
:scheme: https
accept: application/json, text/plain, /
accept-encoding: gzip, deflate, br
accept-language: zh-CN,zh;q=0.9
cache-control: no-cache
content-length: 325
content-type: application/x-www-form-urlencoded;charset=UTF-8
cookie: PLATEFORMC=ae; COCODE_AE=ae; PLATEFORML=en; c_Ident=15349883665889; s_fid=79AB962980789693-0187C28FB2F543C4; ab.storage.deviceId.2e4ae497-9aed-4a69-8a2d-91cd396ab384=%7B%22g%22%3A%224ad0c315-7a95-c21e-189f-a00d60fbbb7e%22%2C%22c%22%3A1534988369909%2C%22l%22%3A1534988369909%7D; _ga=GA1.2.546492056.1534988373; __gads=ID=088fbe71ec00d1b9:T=1534988375:S=ALNI_MYwWze-JpDhVgLk2t5ckQ_cN2F_TA; cto_lwid=dab74b02-330c-42a5-9d36-8558347551cb; optimizelyEndUserId=oeu1539745964092r0.11418005455424574; optimizelyBuckets=%7B%7D; _ga=GA1.1.546492056.1534988373; ab.storage.deviceId.dde4157a-6ed4-4e47-a940-cdd336f179b2=%7B%22g%22%3A%22c04c03b4-1478-17a3-fb29-d97324875588%22%2C%22c%22%3A1539745966917%2C%22l%22%3A1539745966917%7D; s_cc=true; s_source=%5B%5BB%5D%5D; cmgvo=Typed%2FBookmarkedTyped%2FBookmarkedundefined; s_ev21=%5B%5B%27Natural%2520Search%27%2C%271534988372649%27%5D%2C%5B%27Typed%2FBookmarked%27%2C%271539746164434%27%5D%5D; s_ev22=%5B%5B%27Natural%2520Search%253A%2520Baidu%253A%2520Keyword%2520Unavailable%27%2C%271534988372649%27%5D%2C%5B%27Typed%2FBookmarked%253A%2520HomePage%27%2C%271539746164435%27%5D%5D; VT_HDR=2; idc=16184445; ab.storage.userId.2e4ae497-9aed-4a69-8a2d-91cd396ab384=%7B%22g%22%3A%2216184445%22%2C%22c%22%3A1539746196470%2C%22l%22%3A1539746196470%7D; SCAUTT=BEARER; optimizelySegments=%7B%22182429971%22%3A%22referral%22%2C%22182476429%22%3A%22false%22%2C%22182494213%22%3A%22gc%22%7D; _gid=GA1.1.779890490.1540453689; formisimo=zA775h2lpdp6NtKHaHIEpJCQgd; _gid=GA1.2.1022938571.1540453881; s_campaign=NA%3ANA%3ANA%3ANA%3ANA%3ANA%3ANA%3ANA%3ANA%3ANA%3ANA%3ANA%3ANA%3ANA%3ANA%3Aae%3Aen%3ANA%3ANA%3ATab-Bookarked%3Afree; PHPSESSID=b22mbohdmk57qscsujbiv0dq0d4pp8ju; SCXAT=3HWrwJLXPXGe5YcNuo8M0DQIXOLI73wpPeXrOl8tSPM+1540501370639; s_vs=1; s_dl=1; remember_key=60879632d672d5167b313231660db7c1; is_logged_in=1; CARTID=15195280635a92287f547d0; customer_logged_in=0; ab.storage.sessionId.2e4ae497-9aed-4a69-8a2d-91cd396ab384=%7B%22g%22%3A%220f76d955-ac87-77a8-df13-85896d0caa82%22%2C%22e%22%3A1540503948783%2C%22c%22%3A1540501398853%2C%22l%22%3A1540502148783%7D; s_ppvl=HomePage%2C10%2C10%2C655%2C1366%2C655%2C1366%2C768%2C1%2CP; s_depth=10; s_sq=soqdev%3D%2526c.%2526a.%2526activitymap.%2526page%253DHomePage%2526link%253DSell%252520with%252520Us%2526region%253DinnerWrap%2526pageIDType%253D1%2526.activitymap%2526.a%2526.c%2526pid%253DHomePage%2526pidt%253D1%2526oid%253Dhttps%25253A%25252F%25252Fsell.souq.com%25252F%2526ot%253DA; SCAUAT=eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIxNjE4NDQ0NSIsImZpcnN0TmFtZSI6IllhbiIsImxhc3ROYW1lIjoiWGllIiwicm9sZSI6IlNFTExFUiIsInNlbGxlcklkIjoxLjYxODQ0NDVFNywiZ3JvdXBJZCI6MS42MTg0NDQ1RTcsImlzcyI6IlNPVVEiLCJpZCI6MS42MTg0NDQ1RTcsImV4cCI6MTU0MDU4ODk4OCwidXNlck5hbWUiOiJVYnV5LXVhZSIsImlhdCI6MTU0MDUwMjU4OH0.vijDdv5s0coXK-n4DV5Ow6vLGWzLhLZJODNhTJlgFAAZVJ_ECiiwY2lmEVtmfJZPrGaWq4UKIRlouJ4jw6FPSQFdiiUnnI3XQbQTM8nN4V3pmhZL0ENU61xjkl_l8j6SIdCCPJt4E_aAeGmVapiuBMlUSapEFEmcoAAnL_aP32dz0zRzfERyu3LtHcfI1D9SIBAryGqc2pnsMzvg3OM2JeaMBofgNzG4Ro2EfD03DMjy2JilgxRyxghtteS0N88iOi9CrC6GQoXigr_hX2kN5wPfeMONBAJaTNXGJBUFH_fHxkwsGLkBfAQ2S26qjQykxsND0rOZcaVxVbfgKUieHEczjytBl32o–BV0ZAxUKrYkFLPir16aG5cfa_7CENv_rXKfxyps8ZhoJ7r4mj4utPSEQ-8AQIvzO1j7FDhipAiAw5vjKsGqHpcjmbHKEVQZjOQ3zx0RtnbDvqbQFGxtvwfxAbo0DYQh-RwengohrdAPe4gd_gfolofC5x9dVwHV_PG-ydEXa9nyUfM8jR2qu1FW0ymU6vRKTRBcVMtWfTUr6LVaaHj8Pro82xPfOyXMYdt0Y-VBErTHUWeZFveQn6HqN5wO_tw4vi7QgMP1lH7D1ptZQgALrPhv7Uac-V_ZgEWtxmGdIsQnbLVA1BgDUjcmlX5fiKkpc__UFCRM80; SCAURT=31120e270797f9d3b5f7da1a53493a52; ab.storage.sessionId.dde4157a-6ed4-4e47-a940-cdd336f179b2=%7B%22g%22%3A%22a70246b5-4dd8-5c31-1cb9-6ed1cdb1960a%22%2C%22e%22%3A1540504386313%2C%22c%22%3A1540501385158%2C%22l%22%3A1540502586313%7D; _gat=1; s_nr_lifetime=1540503851189-Repeat; s_nr_year=1540503851190-Repeat; s_nr_quarter=1540503851191-Repeat; s_ppv=HomePage%2C10%2C9%2C655%2C1366%2C655%2C1366%2C768%2C1%2CP
origin: https://sell.souq.com
pragma: no-cache
referer: https://sell.souq.com/orders/order-management?tab=confirmed
user-agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36
x-requested-with: XMLHttpRequest

data[0][0]: 3645400471
data[1][0]: 1250900376
data[2][0]: 231834900488
data[3][0]: 16000600468
data[4][0]: 4980800471
data[5][0]: 228639400488
data[6][0]: 4980600471
data[6][1]: 10055900471
data[7][0]: 5218600302
token: 3HWrwJLXPXGe5YcNuo8M0DQIXOLI73wpPeXrOl8tSPM

这就是 token,也就是针对数据报文的签名。是否前面返回的,我再仔细看一下。


关于是否是 CSRF_TOKEN 的问题。
是这样的,我注意到服务器有回复一个参数:
gSouqCsrfToken = “058e5fa7fff29647758beb38e6f5668a”;
但此时,网站提交出去的 token 是:
token: Q3i6L7GD2HzjlrBN3CrzEe8NEj1wIOYbYT5ZvV43RL4
但同时我注意到,这个 token,在很多次请求中,都在放在( Request Header)中。并不是 response header 中。
cookie: SCXAT=Q3i6L7GD2HzjlrBN3CrzEe8NEj1wIOYbYT5ZvV43RL4+1541574632410;
所以,我只能认为它是由 js 来生成的,并不是后端返回的。
但 js 代码中,我并没有发现计算 token 值的地方。

能自己加 header 的 AFAIK 只有 XHR/fetch,都是 JS 控制的,也许你没有找到

js 一步步调试,看从哪里取的

,js 中确实有。但我看不太明白。
var Service=function(e){this.$http=e.get("$http"),this._config={method:“GET”,timeout:60},this.method=function(e){return this._config.method=e,this},this.url=function(e){return-1!==e.indexOf(“https://”)||-1!==e.indexOf(“http://”)?this._config.url=e:this._config.url=gBaseUrl+e.replace(/^//,""),this},this.params=function(e){return this._config.params=e,this},this.data=function(e){return this._config.data=e,this},this.config=function(e){return this.config=.extend(this.config,e),this},this.defer=function(){if(.isUndefined(this._config.url))throw new Error(“No URL in Service definition”);var t=e.get("$cookies");return this._config.data||(this._config.data={}),t.get(“SCXAT”)&&(this._config.data.token=t.get(“SCXAT”).split("+")[0]),this.$http(this._config)}
}

这段 js 代码获取 SCXAT。看起来是从后台的响应中取出来的,但我用调试工具,没看到后台响应中有给出来。
或许是我没看懂。

这个是用来防 csrf 攻击的,真不知道怎么弄,标记下,看看高手怎么解决

this._config.data.token=t.get(“SCXAT”).split("+")[0]

看这段代码好像是去 cookies 中取出 SCXAT

回到顶部