fix(dy): 补上 detail 接口的 uifid/verifyFp 风控参数

抖音 detail 接口的 Argus 风控要求 uifid / verifyFp / fp 三个参数,缺一则直接
403:响应体为 "Blocked by ArgusSecurityPlugin Uifid Not Found",补上 uifid 但
verifyFp 不对时换成 "... Signature Not Found"。原实现只传 aweme_id,于是
get_aweme_detail 全线失败,详情和媒体都拿不到。

- 三个参数取自浏览器 cookie:uifid 用 UIFID(缺失时退到 UIFID_TEMP),
  verifyFp/fp 用 s_v_web_id。必须用 cookie 里的 s_v_web_id——实测 uifid 搭配
  自生成的 verifyFp 会被判 Signature Not Found,两者需要同源
- 新增 tests/test_douyin_aweme_detail_params.py(不发网络请求)

验证:抓包比对真实浏览器发出的 detail 请求,参数逐项一致;真实爬取中详情与
74MB 视频均下载成功。
This commit is contained in:
程序员阿江(Relakkes)
2026-09-18 17:00:11 +08:00
parent 281b445f52
commit cf513e70a1
2 changed files with 120 additions and 1 deletions

View File

@@ -214,7 +214,19 @@ class DouYinClient(AbstractApiClient, ProxyRefreshMixin):
:param aweme_id:
:return:
"""
params = {"aweme_id": aweme_id}
# 抖音 detail 接口的 Argus 风控要求这两个参数成套出现,缺一则直接 403
# (响应体为 "Blocked by ArgusSecurityPlugin Uifid Not Found"
# uifid = UIFID cookie没有时退到 UIFID_TEMP
# verifyFp / fp = s_v_web_id cookie
# 必须用 cookie 里的 s_v_web_id实测 uifid 搭配自生成的 verifyFp 会被判成
# "Signature Not Found",两者同源才能通过。
s_v_web_id = self.cookie_dict.get("s_v_web_id", "")
params = {
"aweme_id": aweme_id,
"uifid": self.cookie_dict.get("UIFID") or self.cookie_dict.get("UIFID_TEMP", ""),
"verifyFp": s_v_web_id,
"fp": s_v_web_id,
}
headers = copy.copy(self.headers)
del headers["Origin"]
res = await self.get("/aweme/v1/web/aweme/detail/", params, headers)

View File

@@ -0,0 +1,107 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2025 relakkes@gmail.com
#
# This file is part of MediaCrawler project.
# Repository: https://github.com/NanmiCoder/MediaCrawler/blob/main/tests/test_douyin_aweme_detail_params.py
# GitHub: https://github.com/NanmiCoder
# Licensed under NON-COMMERCIAL LEARNING LICENSE 1.1
#
"""抖音 aweme detail 接口的风控参数回归测试。
回归背景:上游 detail 接口的 Argus 风控要求 ``uifid`` / ``verifyFp`` / ``fp``
三个参数,缺一时直接 403响应体为
``Blocked by ArgusSecurityPlugin Uifid Not Found``(补上 uifid 但 verifyFp 不对时
换成 ``... Signature Not Found``)。原先只传 aweme_id于是详情全线失败
连带媒体也拿不到。
这里不发起任何网络请求,只锁定这三个参数确实被带上、且取值来自浏览器 cookie。
"""
from __future__ import annotations
import pytest
from media_platform.douyin.client import DouYinClient
AWEME_ID = "7525538910311632128"
class _StubPage:
"""占位 page仅用于构造 client本测试不会走到 playwright 调用"""
async def evaluate(self, expression): # noqa: ANN001
return {}
def _make_client(cookie_dict: dict) -> DouYinClient:
return DouYinClient(
headers={
"User-Agent": "test-user-agent",
"Cookie": "a=1",
"Origin": "https://www.douyin.com/",
},
playwright_page=_StubPage(),
cookie_dict=cookie_dict,
)
@pytest.mark.asyncio
async def test_aweme_detail_sends_uifid_and_verify_fp(monkeypatch):
"""uifid 与 verifyFp/fp 必须成套出现,且都取自 cookie"""
captured: dict = {}
async def fake_get(uri, params=None, headers=None): # noqa: ANN001
captured["uri"] = uri
captured["params"] = dict(params or {})
return {"aweme_detail": {"aweme_id": AWEME_ID}}
client = _make_client(
{"s_v_web_id": "verify_test_fp", "UIFID": "uifid-from-cookie"}
)
monkeypatch.setattr(client, "get", fake_get)
await client.get_video_by_id(AWEME_ID)
assert captured["uri"] == "/aweme/v1/web/aweme/detail/"
assert captured["params"]["aweme_id"] == AWEME_ID
assert captured["params"]["uifid"] == "uifid-from-cookie"
# verifyFp 与 fp 必须同源,且用 cookie 里的 s_v_web_id自生成的会被判 Signature Not Found
assert captured["params"]["verifyFp"] == "verify_test_fp"
assert captured["params"]["fp"] == "verify_test_fp"
@pytest.mark.asyncio
async def test_aweme_detail_falls_back_to_uifid_temp(monkeypatch):
"""没有 UIFID 时退到 UIFID_TEMP"""
captured: dict = {}
async def fake_get(uri, params=None, headers=None): # noqa: ANN001
captured["params"] = dict(params or {})
return {"aweme_detail": {}}
client = _make_client({"s_v_web_id": "fp", "UIFID_TEMP": "temp-only"})
monkeypatch.setattr(client, "get", fake_get)
await client.get_video_by_id(AWEME_ID)
assert captured["params"]["uifid"] == "temp-only"
@pytest.mark.asyncio
async def test_aweme_detail_without_cookies_still_requests(monkeypatch):
"""cookie 缺失时不能抛异常,参数退化为空串(由服务端决定是否放行)"""
captured: dict = {}
async def fake_get(uri, params=None, headers=None): # noqa: ANN001
captured["params"] = dict(params or {})
return {"aweme_detail": {}}
client = _make_client({})
monkeypatch.setattr(client, "get", fake_get)
await client.get_video_by_id(AWEME_ID)
assert captured["params"]["uifid"] == ""
assert captured["params"]["verifyFp"] == ""
assert captured["params"]["fp"] == ""