简介:资源面向需要逆向Shopee泰国站点API、构建跨境电商数据采集能力的开发者,提供一套可运行的全套源码与说明文档。压缩包共23个文件,包含Frida Hook核心JavaScript脚本、Python服务端与爬虫客户端、JSON参数配置、数据库文件及辅助说明,整体仅43KB,结构清晰。方案通过Frida Hook动态获取APP加密参数,利用RPC将参数生成逻辑暴露给外部调用,实现请求参数的实时复用,并配套执行流程优化、反爬对抗策略、风险控制及扩展功能建议。已有518人学习,适合具备一定逆向基础、希望快速搭建可维护爬虫系统的工程师参考使用。 做了两年多东南亚电商数据分析,我最大的体会是:数据能拿到多少,分析就能做多深。尤其是做Shopee泰国站相关业务时,这个感受更深。前阵子接了一个竞品动态监控项目,需要持续跟踪一批店铺的价格、销量、评价变化。最开始我想走正路——去Shopee开放平台申请API。结果翻遍文档,开放能力基本围绕商家后台的订单、商品上下架、物流协同这些场景,商品历史价格、评价内容、销量趋势这类分析型数据,官方根本不对个人开发者开放,就算有企业资质,审批流程也够你喝一壶。这条路堵死之后,我才正式启动了API逆向方案。
所谓"Shopee泰国API逆向",简单说就是绕过客户端,用自己的程序去请求App背后的接口,把签名、token、请求头完整复刻出来,实现类似App的数据获取能力。我花了大概三周时间,从抓包、反编译、签名还原到搭出一套可运行的Python工程,现在这台机器上每天还在稳定跑。这篇东西我尽量把思路讲透,而不是贴一堆跑不了的代码。适合三类人看:做选品和价格监控的数据分析师、想研究现代电商API签名机制的后端工程师、以及刚接触安卓逆向想找一个完整案例的初学者。如果你以为拿到源码就能永久躺平,那趁早打消这个念头,这类方案永远在维护的路上。
1. 先弄清一个问题:官方API为什么给不了你想要的数据
1.1 开放平台到底开放了什么
Shopee开放平台并非没有API,相反,它有一套非常完整的接口体系,覆盖店铺管理、商品上新、订单出库、客服会话等商家运营场景。文档写得相当规范,鉴权流程、请求示例、错误码表一应俱全,用起来比很多国内平台顺手。问题从来不在"接口做得好不好",而是"它允许你碰什么"。你翻到接口列表就会发现,不少接口标注着"邀请制"或"白名单",点进去只有一句"请联系您的客户经理"。个人开发者连申请入口都找不到,更别提拿到密钥了。
以我当时的需求来说:商品详情接口能拿到价格和库存,但只返回当前值,没有历史曲线;评价接口只开放给部分高等级卖家,普通账号根本申请不下来;销量趋势这类数据,干脆就没有对应接口。换句话说,官方API是给"运营者"用的,不是给"观察者"用的。当你想做跨店铺、跨品类的数据观察时,开放接口的作用就很有限了。你要么接受拿不到数据的现实,要么换一条路。
1.2 什么场景才需要走逆向
我接触到的需求,基本可以归成三类。第一类是竞品监控,持续跟踪对手店铺的价格调整、上新节奏、历史销量变化,这是电商运营里最普遍的需求。第二类是比价与选品工具,市面上不少做东南亚市场的选品SaaS,数据源就是这些App接口。第三类纯粹是技术研究,想理解现代电商客户端的签名体系、风控策略是怎么设计的,毕竟Shopee这类平台的客户端安全强度,在行业内算比较有代表性的。
需要提醒的是,这三类场景并不都理直气壮。自己做数据分析和学习研究,灰色程度相对低;如果是商业化工具,你需要对自己的数据来源、使用合规性做完整评估。我见过有人把这类方案包装成收费服务卖给商家,结果接口一变动就售后爆炸,既折腾自己也给平台添麻烦。如果你只是写个脚本自己用,把频率和边界控制好,风险会小很多。
1.3 合规边界先划清楚
我个人的原则是三条:只碰自己账号有权限看的数据,比如自己的店铺信息、自己发起的搜索请求;不动用户隐私数据,不做批量采集用户信息的事;不用来刷单、批量注册、伪造评论这类直接影响平台生态的操作。逆向本身是一种技术手段,工具没有原罪,但用在哪里完全由人决定。这篇文章讲的思路,是以学习研究和个人数据获取为前提的,具体场景合规性请自行判断。
2. 泰站请求骨架:抓包看到的那一堆参数,谁是谁
2.1 抓包环境与SSL Pinning处理
动手第一步,先把客户端流量看清楚。我用的组合是mitmproxy加一台Pixel安卓真机,模拟器也不是不行,但真机在风控眼里更接近正常用户,后面遇到的干扰会少一些。Charles我也用过,两者在抓包层面差别不大,选mitmproxy主要是因为它可以命令行跑、方便集成到自动化流程里。
这里有个绕不开的坎:现代Android App基本都做了SSL Pinning,证书校验不通过,HTTPS流量直接断掉。解决办法有两个方向。一个是给系统证书目录安装自定义CA证书,对Android 7以上系统需要在Magisk里用证书模块处理;另一个是用Frida把App里的证书校验函数hook掉,强制信任用户证书。我实际用的是后者,本质是替换TrustManager的checkServerTrusted实现,让它直接放行。几十行脚本,网上模板很多,但要注意不同Android版本对应的hook点略有差异,得自己调试几次。
2.2 从一次真实请求看参数分布
流量通了之后,随便触发一个商品搜索,你会发现每个请求都带着一大堆字段。刚开始看很懵,别急着研究每一个参数,先分个类。
明文参数一般包括:时间戳、分页参数、搜索关键词、区域标识这类业务字段。加密或签名字段通常集中在请求头和个别body字段里。在泰站这边,请求头里有几个字段是你必须重点观察的:一个用于标识设备指纹,一个携带登录态凭证,还有一个就是签名,常见命名是anti-token这一类。Body内部往往还嵌套了一层数据签名,值是一段定长的十六进制字符串,肉眼根本读不出业务含义。
| 字段类别 | 典型字段 | 说明 |
|---|---|---|
| 业务明文 | 关键词、分页、区域标识 | 直接读,用于定位数据 |
| 时间相关 | timestamp | 每次请求都会变,参与签名 |
| 设备相关 | 设备指纹、启动ID | 首次生成后基本固定 |
| 会话相关 | 登录凭证、Cookie | 过期后需重新登录 |
| 签名字段 | anti-token、body签名 | 定长十六进制或Base64,最难还原 |
2.3 动态字段与固定字段的区分方法
面对几十个参数,最快的切入方式不是猜,而是对比。同一个操作,在相同设备上连续发三次请求,把三个请求的参数逐一对齐。你会发现大部分业务参数是固定的,变化的就那么几个:时间戳、随机串、签名值、会话令牌。把"每次都在变"的字段挑出来,逆向的范围一下子就缩小了。
我当时用了一个很笨但很有效的方法:把三次请求的参数整理成三列Excel,按字段名逐行标绿标红。绿色是固定的,红色是变化的。最后剩下大概五六个红色字段,再逐个去APK里搜它们的生成逻辑。这个环节省不了,因为每个字段背后都是一段代码,定位顺序决定了你的工作效率。先处理时间戳这种一眼能看懂的,最后啃签名,心态会稳很多。
3. 签名算法还原:从Jadx反编译到Frida验证的完整路径
3.1 用Jadx反编译定位签名入口
拿到APK,我习惯先扔进Jadx。这个工具会把DEX字节码还原成可读的Java代码,官方源码里搜不到的东西,在这里基本都能找到。泰站App的包体积不小,反编译需要等一会儿,电脑内存最好给足,否则会卡死。
搜索关键词的顺序很重要。先用签名相关的高频词:"anti"、"sign"、"token"、"secret"、"salt",定位到候选类;再用业务相关的关键词,比如请求路径的片段、参数名,追到具体的构造方法;最后用调用链往上翻,看这个值是在哪个类、哪个方法里被计算出来的。Jadx搜索时记得勾选"搜索所有节点",有些字符串会被拆开拼接,直接搜原始字符串搜不到。
这一通操作之后,你会碰到大量混淆过的类名,看起来像a.b.c这种。混淆很讨厌,但有个规律:真正参与签名的逻辑,往往集中在一两个util类里,而且方法内部肯定会出现字符串拼接、字节数组转换、Hex输出这类操作。按这个特征去筛,候选范围能缩到很小。
3.2 一个典型的签名生成流程
我把定位到的那套逻辑简化之后,大概是这个模式:把所有参与签名的业务参数按字典序排列,拼成一个长字符串,再加入一个固定盐值,最后做哈希运算,转成十六进制。伪代码如下:
import hashlib import time def generate_sign(path: str, params: dict, salt: str) -> str: # 排序拼接是签名算法里最常见的套路 items = [f"{k}={params[k]}" for k in sorted(params.keys())] raw = path + "&".join(items) + salt + str(int(time.time())) return hashlib.sha256(raw.encode("utf-8")).hexdigest()注意,这不是泰站当前版本的完整算法,只是我用来演示的通用骨架。真实算法在拼接顺序、盐值来源、参与字段范围上一定有差异,但思考路径是一样的。你按这个思路去还原自己抓到的包,很快能对上号。另外,不同地区版本可能有不同盐值来源,我就遇到过泰国版和印尼版用的是不同拼接顺序的情况,别拿一套参数硬套全部站点。
3.3 签名之外,服务端还会校验什么
拿到签名规则,只是拿到了入场券,不代表你能畅通无阻。服务端还会校验几个东西:时间窗口,签名里的时间戳如果和服务器时间差超过几十秒,直接拒绝;随机数去重,同一个nonce短时间内不能重复出现,防止重放攻击;设备指纹,同一套签名换一个设备标识,风控等级会骤升。设备指纹通常会在首次启动时生成,存在本地存储里,之后每次请求从存储读取。你直接在代码里伪造一个不存在的设备标识,短期内没问题,长期一定会触发风控。
我最初以为把签名还原出来就完事了,结果用Python直接请求,连续被拒。后来才发现,签名只是整个安全链路的一环,前面那几个校验点一个都不能漏。这也是为什么很多网上的逆向方案"跑几天就失效",他们只还原了最表层的一部分。签名、设备、时间、会话,四者必须像一个真实用户那样协同工作。
4. 源码工程怎么组织:签名器、请求层与频率控制的落地写法
4.1 模块划分与目录结构
逆向方案和普通业务代码最大的区别是:它会被平台更新冲掉。所以工程结构必须以"方便维护"为第一原则,把易变的部分集中隔离。我最终的目录结构是这样:
shoppe-th-api/ ├── config/ │ └── config.yaml # 盐值、请求路径、版本号等易变配置 ├── signer/ │ ├── base.py # 签名器抽象接口 │ └── v1_impl.py # 某个客户端版本的签名实现 ├── client/ │ ├── session.py # 会话管理、Cookie保持 │ └── api_client.py # 业务请求封装 ├── parser/ │ └── response_parser.py # 响应解析 └── main.py # 调度入口核心思路就一句话:签名算法独立成模块,版本号写进目录名,配置全部外置。以后客户端升级,新写一个v2_impl.py就行,不动其他代码。配置外置解决了一个很现实的问题:当平台更换盐值或接口路径时,不需要重新发布代码,改一下yaml就行。我第一次没做配置隔离,结果平台换了个接口路径,我翻了半天代码才找到拼接URL的地方。
4.2 签名器与请求客户端的对接代码
签名器我做成抽象接口,方便不同版本替换:
class BaseSigner(ABC): @abstractmethod def sign(self, method: str, path: str, params: dict, timestamp: int) -> dict: """返回需要注入请求头或body的签名字段""" pass请求客户端在发送前调用签名器,把结果合并进请求头:
class ApiClient: def __init__(self, signer: BaseSigner, session: requests.Session): self._signer = signer self._session = session def get_product(self, item_id: str): path = "/api/v2/product/get" params = {"item_id": item_id} timestamp = int(time.time()) sign_fields = self._signer.sign("GET", path, params, timestamp) headers = {"x-time": str(timestamp), **sign_fields} resp = self._session.get( "https://shopee.co.th" + path, params=params, headers=headers, ) return resp.json()注意一个细节:签名的计算必须在发送前那一刻完成,不能提前几秒算好放缓存里,因为时间戳字段一旦过期,服务端校验直接不通过。另外,签名器里参与签名的字段列表要明确写出来,不要用字典遍历所有params,服务端对字段顺序和遗漏很敏感,少一个字段算出来的签名就是错的。
4.3 会话保持、频率控制与重试策略
请求层最容易被忽略却又最关键的是频率控制。我见过很多人把签名搞定后,用for循环疯狂发请求,结果跑了十分钟账号就被风控了。正确做法是:所有请求走同一个requests.Session,登录态的Cookie集中管理;每次请求之间加随机延迟,基础间隔1到3秒,再叠加一个随机抖动;遇到429、503这类响应,用指数退避重试,最多三次,退避间隔从1秒开始,每次翻倍。这样既保证了数据时效性,又不至于触发风控。
会话方面,泰站登录态的过期策略比较激进,我遇到过几次请求返回419的情况。这个状态码基本代表会话失效,处理方式很直接:重新走一遍登录流程,更新Cookie,再继续任务队列里未完成的请求。Session的User-Agent、Accept等头要尽量和真实客户端一致,我见过只改了User-Agent但漏了Accept的,结果被风控识别成脚本,请求全返回验证码。这些细节平时不起眼,一旦触发风控,排查起来很头疼。
5. 运行实测避坑:签名失效、会话过期与版本升级的应对
5.1 高频报错对照表
跑了几个月,把遇到的高频问题整理了一下:
| 报错或现象 | 可能原因 | 处理方式 |
|---|---|---|
| 401 Unauthorized | 签名错误或token过期 | 检查签名拼接顺序和时间戳 |
| 403 Forbidden | 设备指纹异常或触发风控 | 降低请求频率,检查设备标识是否被标记 |
| 419 Page Expired | 登录会话失效 | 重新登录刷新Cookie |
| 429 Too Many Requests | 请求频率过高 | 增大延迟,指数退避 |
| 签名无效但不报错,返回空数据 | 时间戳偏差 | 校准本地时间,检查时区换算 |
5.2 一次"签名无效"的完整排查实录
说一个我印象很深的坑。某天脚本突然大面积报签名无效,我第一反应是平台更新了算法。于是重新抓包、对比参数、检查代码,折腾了两个小时,最后发现是我服务器的系统时间出问题了。
为什么时间会影响签名?因为签名里包含时间戳,服务端校验时会对比签名里的时间戳和服务器当前时间。我服务器时区设置成了UTC,本地代码里却按东八区生成了时间戳,两者差了整整八个小时。签名算出来是"昨天的",服务端当然拒绝。那次之后,我在配置里加了一个时钟同步检查,启动任务前先校准本地时间,再校验与目标站点的时区偏移。泰站用的是UTC+7,这个偏移量也写进了配置。
这个排查过程也让我养成了一个习惯:遇到签名失效,先看时间再看代码。具体来说,先用NTP同步服务器时间,确认系统时区配置,再检查代码里时间戳生成的时区参数,最后才去怀疑算法本身。顺序反了,很容易浪费大量时间。
5.3 客户端版本升级后的维护策略
平台客户端每隔一段时间就会强制升级,升级之后旧的接口路径大概率还能访问,但签名算法的细节可能悄悄变化。维护策略就一条核心原则:不要追新,先存量后增量。
新版本发布后,先别急着升级自己的工程。旧版本App只要还能用,就让线上任务继续跑,同时用备用设备装新版本,抓包对比新旧请求的差异。确认哪些字段变了、签名算法变了多少,再写新的签名器实现,小流量切换验证没问题,最后才全量替换。另外,版本升级时最大的变动往往不是签名算法本身,而是参与签名的字段集合变了,比如多了一个category_id或者少了一个region字段。这种情况排查起来比算法变更更隐蔽,因为你光看签名代码发现不了问题,必须逐字段比对请求参数。
这套流程看起来慢,但能保证线上任务不断档。我吃过一次亏,当时贪快直接换新算法,结果漏了一个字段,导致一整个任务队列的数据全部作废,重跑花了三天。从那以后,任何算法变更都走灰度流程。
最后分享两个小经验。一个是抓包数据一定要留档,每次逆向的请求参数、签名结果、对应版本号都存下来,哪天算法变了,翻旧数据比对差异效率极高。另一个是别把所有逻辑堆在一个文件里,我一开始图省事,签名、请求、解析全写在main.py里,后面维护的时候想死的心都有。重构之后,换一个客户端版本只需要新增一个签名器实现类,改几行配置,其他代码基本不动,这才是"可运行源码"该有的样子。
本文还有配套的精品资源,点击获取