虎牙登录: 接入hydevice高信任设备指纹与App场景验证支持

- 新增 fingerprint/: node仿真Android WebView运行官方hydevice.js(含WASM加密), 产出与真机同构的高信任sdid
- 新增 device_fingerprint.py: py编排node runner获取sdid, 失败自动降级旧无指纹流程
- login.py: prepare_device优先高信任指纹; 增加App场景常量(appId=5008, appSign=md5(appId+verCode+appKey)[:8], 逆向自APK UdbNetHelper)
- solver: 支持appId/page_url/use_touch_events参数化(App模式); get3使用csid会话id; 识别qr_auth扫码(冷却)与dx_auth短信策略并抛专用异常
This commit is contained in:
yml2213
2026-08-23 17:32:34 +08:00
parent bf2f78aa70
commit 06a0a1e7d3
8 changed files with 438 additions and 16 deletions
+11 -1
View File
@@ -1,8 +1,18 @@
"""虎牙风控验证求解器。"""
from .solver import HuyaVerificationError, HuyaVerificationSolver, solve_huya_verification
from .solver import (
HuyaNoSessionError,
HuyaQrAuthRequiredError,
HuyaSmsAuthRequiredError,
HuyaVerificationError,
HuyaVerificationSolver,
solve_huya_verification,
)
__all__ = [
"HuyaNoSessionError",
"HuyaQrAuthRequiredError",
"HuyaSmsAuthRequiredError",
"HuyaVerificationError",
"HuyaVerificationSolver",
"solve_huya_verification",
+79 -11
View File
@@ -7,6 +7,7 @@ import io
import json
import random
import time
import uuid
from collections.abc import Mapping
from http.cookies import SimpleCookie
from urllib.parse import parse_qs, quote, urlparse
@@ -33,6 +34,22 @@ class HuyaVerificationError(RuntimeError):
"""虎牙风控验证失败。"""
class HuyaNoSessionError(HuyaVerificationError):
"""config3 未下发图形验证会话(策略不匹配或无需滑块)。"""
class HuyaQrAuthRequiredError(HuyaVerificationError):
"""风控策略为扫码验证(qr_auth),需已登录设备确认,无法自动化。"""
class HuyaSmsAuthRequiredError(HuyaVerificationError):
"""风控策略为短信验证(dx_auth),需接码/收码后提交。"""
def __init__(self, challenge_payload: dict | str, message: str = ""):
self.challenge_payload = challenge_payload
super().__init__(message or "虎牙风控要求短信验证")
def _cookie_mapping(cookie: Mapping[str, str] | str | None) -> dict[str, str]:
"""兼容 dict 和 Cookie 字符串。"""
if not cookie:
@@ -56,6 +73,9 @@ class HuyaVerificationSolver:
ocr: HuyaCaptchaOcr | None = None,
proxies: Mapping[str, str] | None = None,
timeout: tuple[float, float] = (8, 20),
app_id: str = "5002",
page_url: str | None = None,
use_touch_events: bool = False,
):
self.cookie = _cookie_mapping(cookie)
self.ua = ua or DEFAULT_UA
@@ -87,6 +107,9 @@ class HuyaVerificationSolver:
self.strategy_url = ""
self.js_ctx = None
self.behavior_events: list[dict] = []
self.app_id = app_id
self.page_url = page_url or self._auth_page_url()
self.use_touch_events = use_touch_events
@staticmethod
def _auth_page_url() -> str:
@@ -182,16 +205,26 @@ class HuyaVerificationSolver:
"""
self.js_ctx = execjs.compile(js_data.replace("$$self_js$$", dx_js))
def _build_common_payload(self, extra: dict) -> dict:
def _build_common_payload(self, extra: dict, info_override: str | None = None) -> dict:
payload = {
"urlParamMap": self.url_param_map,
"wupData": "",
"page": self._auth_page_url(),
"page": self.page_url,
"behavior": self._encode_behavior(),
"info": self.sdid,
"info": info_override if info_override is not None else self.sdid,
}
payload.update(extra)
return {"appId": "5002", "data": payload}
return {"appId": self.app_id, "data": payload}
def _csid(self) -> str:
"""App WebView 场景 get3 使用 csid_ 前缀会话 id。"""
return f"csid_{uuid.uuid4().hex}"
def _pointer_action(self, action: str) -> str:
"""web 用 mouse 事件,App WebView 场景用 touch 事件。"""
if not self.use_touch_events:
return action
return {"mousedown": "touchstart", "mousemove": "touchmove", "mouseup": "touchend"}.get(action, action)
def _encode_behavior(self) -> str:
"""编码浏览器行为,失败重试时会携带前一次点击记录。"""
@@ -233,6 +266,31 @@ class HuyaVerificationSolver:
self.url_param_map.get("mobile", ""),
len(self.url_param_map.get("param", "")),
)
strategies = []
if isinstance(challenge_payload, dict):
strategies = (challenge_payload.get("data") or {}).get("strategys") or []
if strategies:
try:
logger.warning(
"虎牙登录风控strategys完整结构: {}",
json.dumps(strategies, ensure_ascii=False)[:1200],
)
except Exception: # noqa: BLE001
pass
strategy_url_lower = strategy_url.lower()
is_qr_auth = "qr_auth" in strategy_url_lower or any(
isinstance(s, dict) and str(s.get("strategy")) == "64" for s in strategies
)
if is_qr_auth and "dx_auth" not in strategy_url_lower:
raise HuyaQrAuthRequiredError(
"虎牙风控要求扫码验证(qr_auth/strategy=64),需已登录App设备确认,"
f"账号: {self.url_param_map.get('mobile', '?')}"
)
if "dx_auth" in strategy_url_lower:
raise HuyaSmsAuthRequiredError(
challenge_payload,
f"虎牙风控要求短信验证(dx_auth),手机号: {self.url_param_map.get('mobile', '?')}",
)
self.behavior_events = []
def _restore_slider_background(self, bg_bytes: bytes, segment: str, huyapk: str) -> bytes:
@@ -290,7 +348,7 @@ class HuyaVerificationSolver:
}
act = self.js_ctx.call("get_act", act_data, self.huyapk)
self.behavior_events.append({
"action": "mousedown",
"action": self._pointer_action("mousedown"),
"id": "",
"txt": "",
"clientX": track_points[0][0],
@@ -329,7 +387,7 @@ class HuyaVerificationSolver:
behavior_list = []
for index, item in enumerate(click_data):
behavior_list.append({
"action": "mousedown",
"action": self._pointer_action("mousedown"),
"id": "",
"txt": txt_list[index] if index < len(txt_list) else "",
"clientX": item[0],
@@ -380,6 +438,8 @@ class HuyaVerificationSolver:
bool(vurl),
len(dx_js),
)
if not (gurl and vurl):
logger.warning("虎牙config3未下发验证会话, 完整响应: {}", json.dumps(config, ensure_ascii=False)[:1500])
if gurl and vurl and dx_js:
break
if index == 0 and self.url_param_map.get("sceneType") != "4":
@@ -390,20 +450,22 @@ class HuyaVerificationSolver:
time.sleep(0.3)
if not gurl or not vurl or not dx_js:
raise HuyaVerificationError(
"config3取参失败: "
data_summary = {k: str(v)[:80] for k, v in (data or {}).items() if k != "js"}
raise HuyaNoSessionError(
"config3未下发验证会话: "
f"returnCode={config.get('returnCode')}, "
f"description={config.get('description') or config.get('message') or ''}, "
f"gurl={bool(gurl)}, vurl={bool(vurl)}, js={bool(dx_js)}"
f"gurl={bool(gurl)}, vurl={bool(vurl)}, js={bool(dx_js)}, "
f"data={json.dumps(data_summary, ensure_ascii=False)[:600]}"
)
self._compile_js(dx_js)
point_payload = self._build_common_payload({"ver": 5})
point_payload = self._build_common_payload({"ver": 5}, info_override=self._csid() if self.use_touch_events else None)
last_result: dict = {}
for index in range(3):
if index:
time.sleep(0.4)
point_payload = self._build_common_payload({"ver": 5})
point_payload = self._build_common_payload({"ver": 5}, info_override=self._csid() if self.use_touch_events else None)
logger.info("虎牙验证失败后重试: {}/3", index + 1)
point = self._get_track_point(gurl, point_payload)
@@ -445,6 +507,9 @@ def solve_huya_verification(
sdid: str = "",
session: requests.Session | None = None,
proxies: Mapping[str, str] | None = None,
app_id: str = "5002",
page_url: str | None = None,
use_touch_events: bool = False,
) -> dict:
"""函数式入口,便于登录流程直接调用。"""
solver = HuyaVerificationSolver(
@@ -453,5 +518,8 @@ def solve_huya_verification(
sdid=sdid,
session=session,
proxies=proxies,
app_id=app_id,
page_url=page_url,
use_touch_events=use_touch_events,
)
return solver.solve(challenge_payload)