Python模拟抖音扫码登录:破解Referer与动态Token校验实战
1. 项目概述模拟登录的“雷区”与“钥匙”搞Python爬虫或者自动化测试的朋友对模拟登录应该都不陌生。这活儿听起来简单不就是用代码模仿浏览器发个请求、填个表单嘛。但真上手去碰像抖音这种体量的App时你就会发现事情远没有想象中那么简单。尤其是当登录方式从传统的账号密码升级到更便捷也更安全的扫码登录时背后的校验逻辑就变得复杂起来。我最近就花了不少时间专门研究用Python模拟抖音的扫码登录流程。目标很明确写一个脚本能自动完成从生成二维码、等待手机扫码、到最终获取登录态比如Cookie或Token的全过程。这听起来是个挺实用的工具无论是用于数据采集的合规化登录还是做自动化测试都有不小的价值。但实际做下来最大的挑战并非来自扫码这个动作本身而是隐藏在HTTP请求背后那些看似不起眼却又至关重要的“关卡”——其中“Referer”校验和动态“Token”机制就是两个最典型的“拦路虎”。简单来说Referer就像是你的“介绍信”。服务器会检查你这个请求是从哪个页面跳转过来的。如果Referer不对或者干脆没有服务器很可能直接拒绝告诉你“来路不明”。而Token这里泛指各种动态令牌如as、mas、cp等则是每次会话的“一次性密码”。它由服务器动态生成蕴含了时间戳、设备信息、请求参数等加密信息用于防止请求被重放或篡改。在抖音的扫码登录流程中从获取二维码开始到轮询扫码状态再到最终确认登录几乎每一个关键请求都离不开正确的Token。所以这个项目本质上是一场“攻防演练”。我们作为“攻”方需要用Python代码完美地模拟出合法客户端的每一个行为细节而抖音的服务器作为“防”方则布下了Referer校验和Token验证等多道防线。成功绕过这些防线你就能拿到通往数据宝库的“钥匙”失败则会反复收到“403 Forbidden”、“参数错误”或者“校验失败”的提示脚本卡在原地动弹不得。接下来我就把自己趟坑、填坑的过程详细拆解一遍。我会先带你看清整个扫码登录的完整链条和核心思路然后深入每个环节告诉你Referer和Token具体藏在哪、怎么拿、怎么用最后再分享一堆我踩过的坑和总结的排查技巧。无论你是想实现类似功能还是单纯对现代Web/App的认证机制感兴趣相信都能从中获得启发。2. 扫码登录流程全貌与核心思路拆解在动手写代码之前我们必须先把抖音扫码登录的“地图”画出来。不能看到个二维码就想着去识别那是舍本逐末。整个流程是一个状态机环环相扣任何一环的请求构造出错都会导致满盘皆输。2.1 标准扫码登录流程分解一个完整的、用户无感的扫码登录流程大致可以分为以下四个核心阶段预登录与二维码获取客户端我们的脚本向服务器发起请求说“我要扫码登录”。服务器会生成一个本次登录会话唯一的token注意这个token和后面提到的动态Token不是一回事这里可以理解为一个会话ID和一个对应的二维码图片URL。同时服务器会建立一个以该token为标识的登录会话并开始等待扫码。二维码展示与用户扫码客户端将获取到的二维码图片展示给用户在脚本中我们可能需要保存为图片文件或者生成一个可访问的链接。用户打开手机抖音App使用“扫一扫”功能扫描这个二维码。登录状态轮询在用户扫码前后客户端需要不断地向服务器询问“那个token对应的二维码被人扫了吗扫的人确认登录了吗” 这个阶段通常采用短轮询Polling或长轮询Long-Polling的方式。服务器会返回不同的状态码比如“等待扫码”、“已扫码待确认”、“扫码成功”或“已过期”。登录确认与凭证获取当轮询接口返回“已扫码待确认”或“扫码成功”状态时客户端需要再发送一个最终的“确认”请求有时这步由服务器在App端确认后自动完成。成功后服务器会返回关键的登录凭证例如包含用户身份信息的Cookie如sessionid或一个可用的Access Token。至此模拟登录完成后续的请求都可以携带这个凭证来访问需要登录的接口。2.2 我们的Python脚本需要模拟什么我们的脚本需要自动化地完成上述所有“客户端”的工作。具体来说要模拟一个真实的浏览器或官方客户端的行为模拟网络请求使用requests或aiohttp库发送HTTP/HTTPS请求。维持会话状态使用requests.Session()对象来保持Cookie使得多个请求之间的上下文如临时Token、Cookie得以传递这是模拟登录的基础。解析响应数据从服务器的JSON响应中提取出token、二维码URL、轮询状态、最终凭证等信息。处理动态参数构造每个请求必需的、由前端或客户端算法生成的动态Token。**伪造请求头**让我们的请求看起来像是从“官方”页面发起的其中 Referer 和 User-Agent 是两个最重要的字段。User-Agent 用来伪装成浏览器或移动设备Referer 则用来告诉服务器“我这个请求是从你们自家的哪个页面来的”。2.3 核心难点聚焦Referer与Token在整个流程中Referer和Token的校验几乎贯穿始终。Referer校验的典型场景获取二维码的接口、轮询登录状态的接口。这些接口通常会检查请求头中的Referer字段其值必须是抖音的某个特定域名下的页面例如https://www.douyin.com/或https://sso.douyin.com/。如果脚本直接请求这些接口而不带Referer或者Referer值是一个明显不合法的地址如本地文件file://或空白服务器会直接返回403或400错误。Token校验的典型场景所有涉及状态变更和用户交互的接口。尤其是在轮询状态和确认登录时请求URL或请求体Body中必须包含一个或多个由特定算法生成的Token。这些Token往往与设备信息、时间戳、请求参数甚至之前的响应数据有关具备“一次性”和“防篡改”的特性。服务器收到请求后会用同样的算法和密钥进行验签不一致则拒绝。注意这里说的“Token”是一个广义概念。在抖音的接口中你可能看到诸如as、mas、cp、_signature等参数名它们都属于这种动态校验Token。不同的接口可能使用不同的参数名和生成算法。理解了这张“地图”和两个“关卡”我们就可以开始着手准备工具并进入具体的实战环节了。3. 环境准备与关键工具解析工欲善其事必先利其器。模拟登录对工具的依赖度比较高选对工具能事半功倍。3.1 Python环境与核心库首先确保你有一个Python 3.7以上的环境。核心库就两个requests用于发送HTTP请求。它简单易用同步阻塞的特性在调试阶段反而更直观。pip install requestsaiohttp(可选用于异步高效轮询)如果你希望轮询状态时更高效不阻塞可以考虑使用异步库。但初期调试建议用requests逻辑更清晰。pip install aiohttp3.2 必备的侦察工具抓包与调试这是本次项目的“眼睛”。不看清客户端和服务器之间到底在传什么一切都是盲人摸象。抓包工具首推Fiddler Classic / Charles这两款是专业的HTTP/HTTPS抓包代理工具。你需要在你运行脚本的电脑上配置它们作为系统代理然后配置手机连接同一Wi-Fi并在手机网络设置中手动设置代理为电脑的IP和抓包工具的端口如8888。接着在手机抖音App里操作一遍完整的扫码登录流程。此时抓包工具会记录下所有网络请求和响应。为什么用手机抓包因为抖音的扫码登录逻辑在App端和Web端可能有细微差别且App端的接口往往是最新、最稳定的。通过抓包App的请求我们能获得最准确的接口地址、参数和头部信息。浏览器开发者工具辅助打开抖音网页版如www.douyin.com尝试进行扫码登录虽然可能最终会跳转App但前期流程可捕获。按F12打开开发者工具切换到Network网络面板。勾选Preserve log保留日志然后进行登录操作。这里可以清晰地看到每个请求的Headers尤其是Referer、Payload请求参数可能包含Token和Response。这对于理解Web端的校验逻辑非常有帮助。实操心得抓包时务必关注请求的完整URL、Headers重点是Cookie, Referer, User-Agent, Content-Type以及请求体Form Data 或 JSON。把一次成功的扫码登录流程中按时间顺序出现的所有相关请求都记录下来这是你编写脚本的蓝图。3.3 请求头Headers的标准化配置在编写脚本时我们需要预先定义一套看起来足够“真实”的请求头。以下是一个基础的模板你需要从抓包结果中提取真实的值来填充它import requests BASE_HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, # 从抓包中获取更真实的UA Accept: application/json, text/plain, */*, Accept-Language: zh-CN,zh;q0.9,en;q0.8, Accept-Encoding: gzip, deflate, br, Content-Type: application/x-www-form-urlencoded, # 注意不同接口Content-Type可能不同 Origin: https://www.douyin.com, Referer: https://www.douyin.com/, # 这是一个关键变量不同接口需要不同的Referer Sec-Fetch-Dest: empty, Sec-Fetch-Mode: cors, Sec-Fetch-Site: same-origin, Connection: keep-alive, }关键点User-Agent尽量使用从抓包中获取的抖音客户端或最新版浏览器的UA。Referer这个字段不能写死它必须根据你当前要模拟的请求所对应的“来源页面”动态设置。例如获取二维码的请求其Referer可能是https://sso.douyin.com/login/?nexthttps%3A%2F%2Fwww.douyin.com。Content-Type需要根据接口实际情况调整。如果是提交表单通常是application/x-www-form-urlencoded如果是提交JSON则是application/json。Cookie初始请求可能不需要但一旦服务器返回了Set-Cookie后续请求就必须通过Session对象自动携带或手动添加到HEADERS中。准备好这些我们就可以开始深入最核心、也是最容易出错的环节了。4. 核心环节一获取二维码与Referer的陷阱这是整个流程的起点也是一个常见的“劝退点”。很多脚本在这里就卡住了因为服务器直接返回403或一个含义模糊的错误。4.1 接口定位与参数分析通过抓包你可以找到一个类似https://sso.douyin.com/get_qrcode/或https://www.douyin.com/passport/generate/qr_code/的接口接口地址可能随时间变化。这是一个GET或POST请求。请求示例来自抓包GET /get_qrcode/?servicehttps://www.douyin.comtokenxxxx_signatureyyyy HTTP/1.1 Host: sso.douyin.com Referer: https://sso.douyin.com/login/?nexthttps%3A%2F%2Fwww.douyin.com User-Agent: Mozilla/5.0 (...) ... 其他Headers响应示例{ data: { qrcode_url: https://p3.douyinpic.com/xxx/yyy.png, token: abcdefghijklmnopqrstuvwxyz123456 }, message: success }关键参数解析service: 通常指登录成功后要跳转的地址。token: 本次扫码登录会话的唯一标识后续轮询都要用它。_signature:这就是一个典型的动态Token。它的值是一长串看似随机的字符由客户端根据特定算法生成。4.2 Referer校验的模拟与破解现在来看请求头。你会发现这个请求的Referer字段非常具体指向了抖音SSO单点登录的一个登录页面。我们的脚本在发送这个请求时必须原封不动地设置这个Referer。错误示范# 错误没有设置Referer或设置了错误的Referer response session.get(qrcode_url, paramsparams) # 很可能返回 403 Forbidden 或类似错误正确做法# 1. 从抓包数据中复制准确的Referer specific_headers BASE_HEADERS.copy() specific_headers[Referer] https://sso.douyin.com/login/?nexthttps%3A%2F%2Fwww.douyin.com # 你的抓包结果 # 2. 使用Session对象它会自动管理Cookie session requests.Session() session.headers.update(specific_headers) # 3. 发送请求 qrcode_api https://sso.douyin.com/get_qrcode/ params { service: https://www.douyin.com, # token 和 _signature 可能需要从其他前置接口或算法生成 } response session.get(qrcode_api, paramsparams) data response.json() qr_token data[data][token] qrcode_image_url data[data][qrcode_url]为什么Referer如此重要这是服务器实施CSRF跨站请求伪造防护和来源验证的一种简单有效的手段。它确保“获取二维码”这个敏感操作只能由它自己的登录页面发起防止恶意网站伪造请求消耗服务器资源。避坑指南如果即使设置了正确的Referer仍然失败请检查Origin头是否也需要设置以及Cookie中是否需要一个初始的、未过期的会话ID有时在访问登录页时就已经种下了。此外Host头也必须与请求的域名一致。4.3 处理二维码图片拿到qrcode_image_url后我们可以用Python下载并展示它。import io from PIL import Image # 下载二维码图片 qr_response session.get(qrcode_image_url) image_data io.BytesIO(qr_response.content) img Image.open(image_data) img.save(douyin_qrcode.png) # 保存到文件 img.show() # 尝试用系统默认程序打开可能在某些环境不工作 print(f请打开抖音APP扫描二维码。登录会话Token: {qr_token})更友好的做法是生成一个本地HTTP链接用浏览器打开或者直接打印出URL让用户手动访问。至此二维码已经成功生成并展示。接下来脚本需要进入等待和轮询阶段而这里的Token校验才是真正的“硬骨头”。5. 核心环节二轮询状态与动态Token的攻防用户扫描二维码后App端会通知服务器。我们的脚本需要不断地询问服务器“我那个token指qr_token现在是什么状态了” 这个询问的接口就是轮询接口。5.1 轮询接口与状态码抓包中你会找到一个类似https://sso.douyin.com/check_qrcode/或/passport/qrcode/check/的接口它是一个POST或GET请求。请求示例POST /check_qrcode/ HTTP/1.1 Host: sso.douyin.com Content-Type: application/x-www-form-urlencoded Referer: https://sso.douyin.com/login/?next... # 注意Referer可能和获取二维码时相同或不同 ... 其他Headers tokenabcdefghijklmnopqrstuvwxyz123456_signaturezzzzzzzzservice...响应示例{ data: { status: 3, // 状态码 description: 二维码已扫描 }, message: success }常见状态码具体值需以抓包为准0或1: 等待扫码 / 二维码未扫描2: 二维码已过期3: 二维码已扫描等待用户手机端确认登录4或5: 扫码成功登录已确认5.2 动态Token的生成与破解核心难点注意看上面的请求参数除了我们已知的qr_token这里可能就叫token又多了一个_signature。这个_signature就是本环节最大的挑战。它通常不是固定的而是根据当前时间戳、请求参数、设备信息、甚至某个固定盐值Salt通过一种不可逆的哈希算法如MD5、SHA1或自定义算法计算出来的。服务器的工作流程客户端生成一个_signature随请求发送。服务器收到请求后使用相同的算法和密钥对收到的参数进行同样的计算生成一个_signature_server。比较_signature和_signature_server。如果一致说明请求参数在传输过程中未被篡改且请求是来自合法的客户端因为算法和密钥是保密的于是处理请求如果不一致则返回“签名错误”。我们的破解思路逆向工程硬刚通过反编译抖音的Android APK或分析其Web端JavaScript找到生成_signature的算法和密钥。这是最彻底的方法但难度高、工作量大且随着版本更新算法可能会变。模拟执行取巧如果Web端使用了JavaScript生成签名我们可以用PyExecJS、js2py或Node.js子进程来直接执行那段JavaScript代码传入参数得到签名。这需要从网页源码或混淆的JS文件中定位到签名函数。复用与捕获实用在抓包过程中同一个登录会话内多次轮询请求的_signature可能是不变的或者变化有规律。你可以尝试直接复用第一次成功请求中的_signature。或者更可靠的方法是在抓包工具中记录下从开始到登录成功整个过程中所有轮询请求的完整URL包含所有参数。然后在脚本中直接使用这个“快照”式的URL进行轮询。只要会话 (token) 未过期且你的请求间隔、顺序与抓包时一致成功率很高。示例代码采用“复用捕获参数”的实用策略import time # 假设我们从抓包中得到了第一个轮询请求的完整URL和参数包含_signature # 注意这个_signature可能只在本次会话中有效 poll_url https://sso.douyin.com/check_qrcode/ poll_data { token: qr_token, # 这是动态变化的每次要用新的qr_token service: https://www.douyin.com, _signature: YOUR_CAPTURED_SIGNATURE_HERE, # 从抓包中复制注意可能需要随token变化 t: str(int(time.time() * 1000)), # 常见的时间戳参数 } # 轮询循环 max_retries 120 # 轮询2分钟 for i in range(max_retries): # 注意每次轮询可能需要更新时间戳参数 ‘t’ poll_data[t] str(int(time.time() * 1000)) # 发送轮询请求 # Referer 同样重要需从抓包中获取该接口对应的正确Referer poll_headers {Referer: https://sso.douyin.com/...} resp session.post(poll_url, datapoll_data, headerspoll_headers) try: result resp.json() status result[data][status] desc result[data][description] print(f轮询 {i1}/{max_retries}: 状态{status} - {desc}) if status in [3, 4, 5]: # 已扫码或成功 print(扫码成功) # 这里可能还需要一个最终的“确认”请求 confirm_url ... # 从抓包找确认接口 confirm_data {...} # 包含token和新的_signature confirm_resp session.post(confirm_url, dataconfirm_data) if confirm_resp.json().get(message) success: print(登录确认成功) # 此时session.cookies中应该已经包含了关键的登录Cookie如sessionid print(获取到的Cookies:, session.cookies.get_dict()) break elif status 2: print(二维码已过期请重新获取。) break # 状态为0或1则继续等待 except Exception as e: print(f轮询请求解析失败: {e}) time.sleep(1) # 每秒轮询一次核心技巧对于_signature这类参数最务实的起步方法是完整捕获一次成功登录流程的所有请求然后在脚本中尽量复现这些请求的顺序、参数和头部。先追求“跑通”再考虑去逆向算法实现“通用”。很多时候一个会话内捕获的_signature是可以复用的。6. 核心环节三登录确认与最终凭证获取当轮询接口返回“已扫码”或“等待确认”状态时并不意味着登录完成。通常还需要一个最终的“确认登录”请求。这个请求的校验往往最为严格。6.1 确认接口的调用这个接口的地址可能类似/passport/qrcode/confirm/或/confirm_login/。它的作用是通知服务器“用户已经在手机App上点击了‘确认登录’请下发最终的登录凭证。”请求特点更强的Token校验这个请求的_signature或其他Token参数如mas,as的生成算法可能更复杂或者需要用到之前响应中返回的某个临时凭证。关键的响应成功响应后服务器通常会通过Set-Cookie头部在会话中设置关键的登录Cookie例如sessionidxxxxxx。同时响应体里也可能直接返回一个access_token或用户基本信息。6.2 凭证的提取与保存登录成功的标志就是获取到了能够代表用户身份的凭证。对于Web Cookie# 使用requests.Session()确认请求成功后Cookie会自动保存在session.cookies中 if login_success: cookies_dict session.cookies.get_dict() # 找到关键的cookie比如sessionid important_cookie cookies_dict.get(sessionid) if important_cookie: print(f登录成功关键Cookie: sessionid{important_cookie}) # 可以将cookies保存到文件供后续脚本使用 import json with open(douyin_cookies.json, w) as f: json.dump(cookies_dict, f)对于返回的Tokenconfirm_resp_json confirm_resp.json() access_token confirm_resp_json.get(data, {}).get(access_token) if access_token: print(f登录成功Access Token: {access_token}) # 后续请求需要在Header中携带Authorization: Bearer {access_token}至此整个模拟扫码登录的流程就走完了。你的脚本现在拥有了一个有效的登录会话可以尝试访问需要登录的接口例如获取用户主页信息、粉丝列表等请注意遵守平台规则。7. 常见问题排查与实战调试技巧即使按照上述步骤操作你也一定会遇到各种问题。下面是我在实战中总结的排查清单和技巧。7.1 问题速查表问题现象可能原因排查思路403 Forbidden / 404 Not Found1.Referer头缺失或错误。2. 请求接口地址已过期或不对。3.Host头不正确。4. IP或请求频率被临时限制。1. 核对抓包数据中的Referer确保完全一致。2. 重新抓包确认接口URL。3. 检查Host头是否与URL域名匹配。4. 更换IP或增加请求间隔。“参数错误” / “签名错误”1. 动态Token如_signature缺失、错误或已过期。2. 请求参数不全或参数名/格式有误。3. 时间戳t参数偏差太大。1. 确认Token生成逻辑或直接复用抓包中的完整参数。2. 对比抓包请求的Query String或Form Data一个参数都不能少。3. 确保时间戳是毫秒级且与服务器时间大致同步。“token无效” / “二维码过期”1. 轮询使用的token与获取二维码时返回的token不一致。2. 二维码确实超时通常2分钟。3. 同一个token被重复确认。1. 检查变量传递是否正确。2. 重新执行流程获取新的二维码和token。3. 确保确认登录请求只发送一次。轮询始终返回“等待扫码”1. 二维码没有被正确扫描。2. 手机App与脚本模拟的登录环境不一致如地区限制。3. 抓包环境如代理干扰了App正常通信。1. 确保生成的二维码能被抖音App成功识别。2. 尝试在同一网络环境下操作。3. 关闭抓包代理用纯脚本环境试试。获取不到最终Cookie/Token1. 确认登录的请求未成功。2. Cookie被设置在了错误的域名下。3. 响应结构解析错误。1. 检查确认请求的返回状态码和消息。2. 打印session.cookies查看所有域名下的Cookie。3. 仔细打印并分析确认请求的完整响应JSON。7.2 实战调试技巧“打印一切”原则在脚本关键节点打印出请求的URL、Headers、Body以及响应的状态码、Headers、Body。这是最直接的调试方式。print(f[Request] URL: {resp.url}) print(f[Request] Headers: {resp.request.headers}) print(f[Request] Body: {resp.request.body}) print(f[Response] Status: {resp.status_code}) print(f[Response] Headers: {resp.headers}) print(f[Response] Text: {resp.text[:500]}) # 打印前500字符对比抓包数据将你脚本打印出的请求信息与抓包工具中成功请求的信息进行逐字逐句的对比。从URL、每个Header的键值对到请求体的每个参数必须完全一致动态Token除外但需确认其位置和名称一致。使用Session对象务必使用requests.Session()。它能自动处理Cookie的重用模拟浏览器会话。很多校验依赖Cookie的传递。参数编码注意当请求体是application/x-www-form-urlencoded时requests的data参数传入字典会自动编码。但如果你需要完全复现抓包中的原始字符串特别是参数顺序可以传入一个已编码的字符串。# 字典形式顺序可能被重排 data {a:1, b:2} # 字符串形式完全控制 data_str a1b2处理时间戳很多签名算法依赖时间戳。确保你生成的时间戳单位是毫秒并且与服务器时间没有巨大偏差。可以使用int(time.time() * 1000)。关于算法逆向如果决定逆向签名算法可以从Web端入手。在浏览器开发者工具的Sources或Network中搜索_signature、as、mas、sign等关键词找到生成它们的JavaScript函数。使用Python的execjs库去执行这些JS代码是相对可行的路径。对于App端则需要更复杂的反编译和Hook技术门槛较高。模拟登录是一个需要耐心和细致观察的过程。每一个错误码、每一次失败的响应都是服务器在告诉你“哪里不对”。对照抓包数据像做拼图一样修正你的脚本最终一定能成功打通整个流程。记住先追求用“复现”的方式跑通一次完整的流程这能给你带来巨大的信心之后再考虑如何将捕获的静态参数替换为动态生成让脚本真正通用化。