虎牙登录: 接入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:
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user