Shopee反爬逆向实战:破解x-sap-ri与x-sap-sec动态加密请求头

📅 2026/7/19 9:28:35
Shopee反爬逆向实战:破解x-sap-ri与x-sap-sec动态加密请求头
1. 项目概述当爬虫遇上Shopee的“铜墙铁壁”做数据采集的朋友尤其是盯上东南亚电商巨头Shopee的最近两年应该没少头疼。以前可能改改User-Agent、控制一下请求频率就能拿到数据但现在你会发现请求商品详情页的API不带上一对名为x-sap-ri和x-sap-sec的请求头服务器根本不理你直接返回403或者一些加密的挑战数据。这对看似简单的请求头背后是Shopee为对抗自动化爬取而筑起的一道动态加密防线。它不像传统的验证码那样有图形界面而是完全隐藏在JavaScript的逻辑里对每一次请求的关键参数进行实时计算和签名。我最近因为一个市场分析项目需要持续获取Shopee特定类目下的商品价格、库存、销量等详情数据不得不正面硬刚这套机制。整个过程就是一个典型的Web逆向工程实战从浏览器中捕捉加密前后的网络请求定位到负责生成这两个头的JavaScript代码块理解其加密逻辑和核心参数最后在Python爬虫环境中复现这一过程。这不仅仅是“找到加密函数”那么简单更涉及到对前端代码混淆的对抗、对加密算法依赖的梳理以及如何稳定地在服务端模拟浏览器环境下的计算。接下来我就把这套“硬骨头”的啃法结合踩过的坑和总结的技巧手把手拆解清楚。2. 逆向目标与核心思路拆解2.1 目标请求头x-sap-ri 与 x-sap-sec 是什么首先我们必须明确要攻克的目标。当你用浏览器正常访问Shopee的商品详情页例如https://shopee.vn/product/...时打开开发者工具的Network面板筛选XHR请求你会发现一个关键的数据接口通常路径包含/api/v4/item/get。这个请求的请求头Request Headers里除了常见的cookie,user-agent外一定会包含x-sap-ri和x-sap-sec。x-sap-ri: 这个头通常是一个较长的、看似随机的字符串可能包含数字、字母和符号。它的核心作用是对本次请求的“唯一性”和“时效性”进行签名。逆向分析后发现它的生成通常依赖于当前时间戳、请求的URL路径、可能的请求体内容以及其他一些浏览器环境生成的随机值或固定盐值。服务器收到后会用同样的逻辑进行验签如果对不上或已过期则判定为非法请求。x-sap-sec: 这个头往往比x-sap-ri更复杂长度也可能更长。它通常是多层加密的最终产物集成了更全面的环境指纹信息。根据我的逆向经验x-sap-sec的生成链可能涉及环境收集收集浏览器/客户端的各类指纹如navigator对象下的userAgent,platform,language,hardwareConcurrency屏幕分辨率时区字体列表通过Canvas探测等。行为证明可能包含证明本次请求是由真实浏览器发起的“证据”例如对页面中某个特定JavaScript函数的执行结果或者对一段挑战代码的运算结果。加密与编码将收集到的信息按特定格式拼接后经过一系列加密算法如SHA256、HMAC、AES等处理最后再进行Base64或Hex编码形成最终的x-sap-sec值。这两个头是相辅相成的x-sap-ri更像一个轻量级的请求签名而x-sap-sec则是一个重量级的客户端环境证明。服务器会同时校验两者任何一方失效都会导致请求被拒。2.2 逆向工程的核心思路与工具选型面对这种前端加密盲目搜索是没用的因为加密逻辑是动态且可能频繁更新的。我们的核心思路是“行为重现”让浏览器告诉我们它是怎么算出来的然后我们在Python中模仿这个过程。工具链选择与理由浏览器与开发者工具Chrome DevTools这是主战场。我们需要用无痕模式避免插件干扰访问目标页面在Network面板中精确找到目标请求查看其请求头和发起该请求的调用栈Initiator。这是定位加密代码入口的关键。代码搜索与断点调试在Sources面板中利用搜索功能CtrlShiftF全局搜索x-sap-ri或x-sap-sec字符串找到它们被赋值的地方。通常它们是在请求被发送前通过XMLHttpRequest.setRequestHeader或fetch的headers参数设置的。在这里打下断点重新触发请求代码执行就会暂停让我们可以一步步跟踪生成逻辑。反混淆工具可选但重要Shopee的前端JavaScript大概率是经过混淆的变量名可能是单个字母逻辑被压缩成一团。直接阅读犹如天书。这时需要用到反混淆工具如astexplorer.net在线分析或者Chrome插件Pretty print代码美化。更高级的如果遇到复杂的代码流平坦化控制流混淆可能需要手动分析或使用专门的去混淆工具如de4js进行初步处理。我的经验是先尝试美化代码如果核心逻辑依然难以追踪再考虑反混淆。Node.js 环境当我们从浏览器中提取出关键的加密函数后需要将其“剥离”出来在一个纯净的环境中运行测试验证我们提取的逻辑是否正确。Node.js是最佳选择因为它天然支持JavaScript并且可以方便地引入一些浏览器环境下的对象如window,document的模拟。Python 执行环境最终落地我们的爬虫最终是用Python写的所以必须将验证过的JavaScript逻辑移植到Python中。这里有几种路径纯Python实现如果加密算法是标准的如SHA256、HMAC、AES且逻辑清晰直接用Python的hashlib,hmac,Crypto等库重写。这是最稳定、效率最高的方式。PyExecJS / js2py如果JavaScript逻辑非常复杂重写成本极高可以考虑用这些库在Python中直接执行JavaScript代码片段。但这会引入额外的依赖和性能开销且在分布式部署时可能带来环境问题。Selenium / Playwright这是“终极”但也是最低效的方案——直接控制一个无头浏览器去访问页面让浏览器自己计算好请求头再发起请求。它绕过了逆向过程适用于加密逻辑极其复杂且变化频繁的场景但资源消耗大速度慢不适合大规模采集。我的选择与建议对于Shopee的这套机制我优先尝试纯Python实现。因为经过分析其核心通常是标准加密算法特定参数重写是可行的。只有当遇到极度混淆、且严重依赖浏览器特定对象如重度依赖window.performance.timing的逻辑时才会考虑用PyExecJS作为过渡或者深入分析后仍用Python模拟那些对象。3. 逆向实战定位与解析加密逻辑3.1 第一步网络抓包与入口定位打开Chrome无痕窗口按F12进入开发者工具切换到Network面板勾选Preserve log。然后访问一个Shopee商品详情页。在纷繁的网络请求中找到那个获取商品核心数据的接口通常是get或get_detail之类的api/v4/item请求。点击这个请求在Headers选项卡的Request Headers部分确认x-sap-ri和x-sap-sec的存在。然后最关键的一步点击Initiator选项卡查看这个请求是由哪段JavaScript代码发起的。你会看到一个调用栈最顶层通常是fetch或XMLHttpRequest.send往下找找到属于Shopee域名下的.js文件并且调用栈指向了某个函数。这个函数附近很可能就是设置请求头的地方。另一种更直接的方法是在Sources面板按CtrlShiftF进行全局搜索搜索关键词setRequestHeader或者直接搜x-sap-ri。如果代码没有过度混淆你可能会直接找到类似x.setRequestHeader(x-sap-ri, g)这样的代码行。在这里打上断点。3.2 第二步断点调试与逻辑追踪打上断点后刷新页面或进行任何能重新触发该请求的操作。代码执行会在断点处暂停。此时在右侧的Scope面板你可以看到当前作用域下的所有变量值。查找参数来源将鼠标悬停在g假设是x-sap-ri的值上或者在下方的Console中输入g并回车查看它的值。更重要的是要看g是怎么计算出来的。通常g会等于某个函数调用的返回值比如g t(e, n, r)。步入函数按F11Step into进入这个函数t的内部。现在你需要像侦探一样一步步跟踪这个函数的参数e, n, r分别是什么它们可能是URL、时间戳、或其他配置对象。函数内部调用了哪些其他函数这些函数是做什么的可能是哈希计算、Base64编码、字符串拼接。有没有引用外部的全局变量或对象这些可能是关键的盐值salt或密钥。记录关键节点在调试过程中随时在Console里用copy()函数复制变量的值或者用纸笔记录下关键函数的输入和输出。例如// 在Console中执行 copy(JSON.stringify({input: e, output: g}, null, 2))这能帮你快速建立多组测试用例用于后续验证。处理混淆代码如果代码被压缩成一行点击代码区域左下角的{}Pretty print美化它。如果变量名是a, b, c你需要根据上下文推断其含义。比如一个函数接收参数a然后执行return CryptoJS.MD5(a).toString()那这个函数显然就是计算MD5。实操心得控制流混淆的应对我遇到过一次Shopee的代码使用了控制流扁平化混淆整个函数体是一个巨大的switch-case循环完全打乱了执行顺序。对付这种情况不急于去混淆先通过断点观察在具体输入下代码的执行流即switch跳转的顺序。记录下这个顺序。动态分析在Console中尝试手动调用这个函数并传入不同的参数观察输出变化从而反推其核心逻辑。关键点提取关注那些执行了实际加密操作如CryptoJS.AES.encrypt或哈希操作如window.crypto.subtle.digest的代码块。混淆通常不会改变这些底层API的调用方式。考虑工具如果实在难以人工分析可以尝试将相关代码片段提取出来在astexplorer.net上分析其AST抽象语法树或者寻找能处理这种混淆的在线工具。但要注意自动化工具可能无法完美还原人工理解始终是核心。3.3 第三步提取并验证加密函数当你跟踪到最里层找到了生成x-sap-ri和x-sap-sec的核心函数后下一步就是把它“抠”出来。创建独立环境在本地新建一个test.js文件。复制函数及其依赖不仅仅是复制那个最终函数还要把它内部调用的所有子函数、以及它依赖的全局变量比如一个特定的salt字符串或者一个window下的配置对象一并复制出来。如果它使用了第三方库如CryptoJS你需要在Node.js环境中安装对应的npm包crypto-js或者更简单——直接找到浏览器中该库的源码把用到的部分函数复制出来。对于标准算法我更推荐用Node.js原生crypto模块或Python库替代而不是搬运庞大的第三方库代码。模拟浏览器环境有些函数可能依赖window.location,window.performance,navigator等。在Node.js中你需要模拟这些对象// test.js global.window { location: { href: https://shopee.vn/product/... }, performance: { timing: { navigationStart: Date.now() } } }; global.navigator { userAgent: Mozilla/5.0 ..., platform: Win32, // ... 其他在加密函数中用到的属性 };运行验证用你在浏览器调试时记录下来的多组输入参数调用你提取的函数将输出结果与浏览器中实际生成的x-sap-ri/x-sap-sec进行比对。必须保证多组数据都能完全匹配才能证明提取成功。4. 核心加密逻辑的Python复现假设经过逆向我们确定x-sap-ri的生成逻辑是SHA256( timestamp : url_path : fixed_salt )的前16位十六进制字符串而x-sap-sec是更复杂的、包含环境指纹的AES加密结果。下面展示如何在Python中复现。4.1 复现 x-sap-ri 的生成import hashlib import time import urllib.parse def generate_x_sap_ri(url_path: str, fixed_salt: str aFixedSaltFromJS) - str: 生成 x-sap-ri 请求头 :param url_path: 请求的API路径如 /api/v4/item/get :param fixed_salt: 从JS中逆向得到的固定盐值 :return: x-sap-ri 字符串 # 1. 获取当前时间戳毫秒级这是关键动态参数 timestamp int(time.time() * 1000) # 2. 按JS中的格式拼接字符串 # 注意逆向时一定要看清拼接顺序和分隔符这里假设是冒号分隔 raw_string f{timestamp}:{url_path}:{fixed_salt} # 3. 计算SHA256哈希 hash_obj hashlib.sha256(raw_string.encode(utf-8)) hex_digest hash_obj.hexdigest() # 4. 取前16位或根据实际JS逻辑调整截取长度和位置 x_sap_ri hex_digest[:16] return x_sap_ri # 测试 if __name__ __main__: test_path /api/v4/item/get result generate_x_sap_ri(test_path) print(fGenerated x-sap-ri: {result}) # 应与浏览器中针对同一时间戳和路径生成的值一致4.2 复现 x-sap-sec 的生成以模拟环境指纹的AES加密为例x-sap-sec通常更复杂。假设我们逆向发现它是将一个包含了多种环境信息的JSON字符串用AES-CBC模式加密然后进行Base64编码。import json import base64 import hashlib from Crypto.Cipher import AES from Crypto.Util.Padding import pad import secrets def generate_client_fingerprint() - dict: 模拟浏览器收集客户端指纹信息 # 这些值需要与你的爬虫想模拟的环境保持一致也可以适当随机化以模拟不同用户 return { ua: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ..., platform: Win32, lang: en-US, tz: Asia/Shanghai, screen: 1920x1080, cores: 8, ts: int(time.time() * 1000), # 时间戳 r: secrets.token_hex(4) # 一个随机数增加唯一性 } def generate_x_sap_sec(api_path: str, aes_key: bytes, aes_iv: bytes) - str: 生成 x-sap-sec 请求头 :param api_path: 请求路径 :param aes_key: 从JS中逆向出的AES密钥需处理成16/24/32字节 :param aes_iv: 从JS中逆向出的AES IV16字节 :return: Base64编码后的加密字符串 # 1. 收集指纹信息 fingerprint generate_client_fingerprint() fingerprint[path] api_path # 将请求路径也加入加密内容 # 2. 将指纹字典转换为JSON字符串 # 注意JS中对象的序列化顺序可能影响最终字符串Python默认无序。 # 如果JS加密对顺序敏感需要使用 sort_keysTrue 或 OrderedDict。 fingerprint_json json.dumps(fingerprint, separators(,, :), sort_keysTrue) # 3. 进行PKCS#7填充AES块大小16字节 padded_data pad(fingerprint_json.encode(utf-8), AES.block_size) # 4. AES-CBC加密 cipher AES.new(aes_key, AES.MODE_CBC, aes_iv) encrypted_bytes cipher.encrypt(padded_data) # 5. Base64编码 x_sap_sec base64.b64encode(encrypted_bytes).decode(utf-8) return x_sap_sec # 关键如何获取 aes_key 和 aes_iv # 这些必须从逆向的JS代码中获得。它们可能是硬编码的字符串也可能是通过更复杂的算法动态生成的。 # 假设我们从JS中找到了 # key_str a1b2c3d4e5f6g7h8 // 16字节字符串 # iv_str i9j0k1l2m3n4o5p6 // 16字节字符串 AES_KEY_FROM_JS ba1b2c3d4e5f6g7h8 # 确保是bytes类型长度16 AES_IV_FROM_JS bi9j0k1l2m3n4o5p6 # 确保是bytes类型长度16 if __name__ __main__: test_path /api/v4/item/get sec generate_x_sap_sec(test_path, AES_KEY_FROM_JS, AES_IV_FROM_JS) print(fGenerated x-sap-sec: {sec[:50]}...) # 只打印前50位太长了重要注意事项密钥与IV的处理在实际逆向中AES_KEY和AES_IV很少是明文字符串。它们很可能被隐藏在代码深处或者由另一个函数动态生成例如通过对一个固定字符串进行哈希变换。你必须跟踪到这两个值的最终来源。有时它们甚至是从第一次请求服务器返回的某个令牌token中衍生出来的这就构成了一个链式验证难度更大。5. 集成到爬虫与请求策略成功复现加密函数后就可以集成到你的爬虫框架如requests,aiohttp中了。import requests import time class ShopeeItemSpider: def __init__(self): self.session requests.Session() # 配置一些固定的请求头 self.session.headers.update({ User-Agent: 你的浏览器UA, Referer: https://shopee.vn/, Accept-Language: en-US,en;q0.9, }) # 你可能需要先访问首页获取初始cookie # self.session.get(https://shopee.vn) def get_item_detail(self, item_id, shop_id): # 1. 构造API URL api_path f/api/v4/item/get api_url fhttps://shopee.vn{api_path} params { itemid: item_id, shopid: shop_id } # 2. 动态生成加密请求头 timestamp int(time.time() * 1000) x_sap_ri generate_x_sap_ri(api_path) # 调用之前写的函数 x_sap_sec generate_x_sap_sec(api_path, AES_KEY_FROM_JS, AES_IV_FROM_JS) # 3. 更新本次请求的特定头 headers { x-sap-ri: x_sap_ri, x-sap-sec: x_sap_sec, # 有时还需要一个时间戳头 x-timestamp: str(timestamp), } # 4. 发送请求 try: resp self.session.get(api_url, paramsparams, headersheaders, timeout10) resp.raise_for_status() # 检查HTTP错误 return resp.json() except requests.exceptions.RequestException as e: print(f请求失败: {e}) # 检查是否是反爬导致的失败如返回403或返回一个包含挑战的HTML if resp.status_code 403: print(触发反爬需要检查加密逻辑是否已更新或环境指纹是否被识别。) return None # 使用爬虫 spider ShopeeItemSpider() data spider.get_item_detail(item_id123456789, shop_id987654321) if data: print(f商品标题: {data.get(item, {}).get(name)})6. 常见问题、风控对抗与排查技巧即使成功复现了加密逻辑在实际运行中仍会碰到各种问题。下面是一些实录的坑和解决方案。6.1 加密逻辑突然失效现象爬虫运行一段时间后开始大量返回403错误。原因密钥/盐值动态更新Shopee可能定期如每天、每小时更换后端用于验签的盐值或AES密钥。你从JS中提取的是静态的自然就失效了。算法升级加密算法或拼接逻辑发生了改变。环境指纹检测你的模拟指纹过于单一或存在破绽如navigator属性不全、缺少某些Web API支持被识别为脚本。排查与解决重新抓包分析立即用浏览器重新访问一次抓取最新的请求对比你生成的x-sap-ri和x-sap-sec与浏览器生成的是否还一致。如果不一致说明逻辑已变。检查JS文件版本查看Network中加载的.js文件URL是否带有版本号或哈希值如app.abc123.js。如果这个哈希值变了说明前端资源已更新需要重新逆向。实施动态密钥获取如果发现密钥是从另一个接口动态获取的你需要先模拟那个接口的请求解析出密钥再用于主请求的加密。这构成了一个请求链。丰富环境指纹检查generate_client_fingerprint函数确保其模拟的信息足够真实和多样。可以考虑使用fake_useragent库随机UA并模拟一套更完整的navigator和screen属性。6.2 请求频率过高导致封锁现象即使加密头正确短时间内发送过多请求也会导致IP或账号被临时封锁。策略严格遵守Robots协议查看https://shopee.vn/robots.txt。设置合理的延迟在请求间加入随机延时如time.sleep(random.uniform(2, 5))。使用代理IP池这是中大规模采集的必备。确保代理IP的质量高匿、稳定并实现IP轮换机制。注意如果x-sap-sec中包含了IP相关的指纹切换IP可能导致加密失效需要确认其生成逻辑是否与IP强相关。模拟用户会话维持一个requests.Session并定期像真实用户一样访问一些列表页、分类页而不仅仅是疯狂调用API。6.3 调试与验证技巧表问题可能原因验证/解决方法生成的x-sap-ri与浏览器不一致1. 时间戳单位错误秒 vs 毫秒2. 拼接字符串的顺序或分隔符错误3. 盐值salt错误或缺失4. 哈希算法或输出截取错误1. 在Console打印JS中的Date.now()与Python的int(time.time()*1000)对比。2. 在JS加密函数入口打桩打印出准备哈希的原始字符串与Python生成的进行逐字符比对。3. 确认盐值是全局常量还是从某个对象属性获取。生成的x-sap-sec与浏览器不一致1. 指纹信息收集不全或格式不对2. AES加密模式、填充方式不对CBC/ECB, PKCS7/ZeroPadding3. 密钥Key或初始化向量IV错误4. 加密后的编码方式不对Base64 vs Hex1. 在JS中在加密前将指纹对象JSON.stringify的结果打印出来。2. 仔细查看JS代码中CryptoJS.AES.encrypt的参数确认mode和padding。3. 这是最关键的必须确保Key和IV的字节序列与JS中完全一致。在JS中用CryptoJS.enc.Utf8.parse(keyStr).toString()查看其WordArray的十六进制表示与Python的key.hex()对比。请求返回403但头信息似乎正确1. 缺少其他必要请求头如referer,accept2. Cookie失效或未携带3. 服务器端有额外的风控策略如请求间隔太短4. 加密逻辑中存在“时间窗口”你的时间戳与服务器时间差太大1. 用工具如curl将浏览器成功请求的所有头信息原样复制发送逐个剔除排查。2. 确保session保持了有效的cookie可能需要先访问首页。3. 增加请求延迟模拟更自然的行为。4. 同步服务器时间或检查JS中时间戳是否经过了某种偏移计算。代码在Node.js中运行正常在Python中失败1. 字符串编码问题UTF-8 vs Latin-12. 加密库的默认行为差异如PyCryptoDome与CryptoJS3. 大整数处理差异JavaScript的Number精度问题1. 在所有字符串操作中显式指定.encode(utf-8)。2. 编写一个简单的测试用相同的输入和密钥分别在Node用crypto模块和Python中运行对比每一步的中间输出如填充后的数据、加密后的字节。6.4 长期维护建议监控与告警爬虫程序应具备监控功能当连续多次请求失败或403比例升高时发出告警提示可能需要重新分析加密逻辑。模块化设计将加密生成部分单独写成模块或类。这样当逻辑需要更新时只需修改这个模块而不影响主爬虫流程。备用方案对于极其重要且实时性要求不高的数据可以准备一个基于Playwright或Selenium的备用爬虫。当逆向方案失效时临时启用备用方案保障数据获取同时给你时间重新进行逆向分析。尊重平台始终将你的请求频率控制在对方服务器可接受的范围内避免对目标网站造成不必要的负担。数据采集应在法律和平台规则的框架内进行。逆向Shopee的x-sap-ri和x-sap-sec是一个典型的动态对抗过程。它没有一劳永逸的解决方案考验的是你的耐心、细心和对Web前后端交互的深刻理解。每一次成功的逆向不仅是为了拿到数据更是对自身技术能力的一次扎实提升。记住核心思路永远是让浏览器告诉你答案然后精准地模仿它。