| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131 |
- # -*- coding: utf-8 -*-
- # Author : Charley
- # Python : 3.12.10
- # Date : 2026/08/21
- """卡玩 App token 抓取脚本。
- MuMu 模拟器(已 root)里卡玩 App 登录后,登录态 JWT 存在 Flutter 的
- SharedPreferences 文件(FlutterSharedPreferences.xml)的 flutter.auth_token
- 字段里。本脚本经 adb 以 root 身份直接读该文件,抠出当前有效的 token。
- 该 token 是 Sa-Token 风格的 JWT(loginId=app_user:xxx + rnStr + HS256),
- 换账号重登会覆盖旧值,所以读到的必然是此刻有效的那一条。请求时按接口实际情况
- 放进请求头(卡玩大概率是 satoken 字段),本脚本返回裸 token,不拼任何前缀。
- 用法:
- python get_token.py # 打印当前 token
- from get_token import get_token # 爬虫里直接调用,每次取最新 token
- """
- import sys
- import subprocess
- # MuMu 12 模拟器的 adb 地址(多开实例端口可能不同,按需改,常见 16384 / 16416 ...)
- MUMU_ADB_ADDR = "127.0.0.1:16384"
- # 卡玩 App 包名
- PACKAGE_NAME = "com.kawan.kawanapp"
- # 当前登录态 token 所在的 Flutter SharedPreferences 文件(只存当前账号的一条)
- PREFS_PATH = f"/data/data/{PACKAGE_NAME}/shared_prefs/FlutterSharedPreferences.xml"
- # 设备端执行的 grep:只抠 flutter.auth_token 这一项的值
- # 原始 XML 形如 <string name="flutter.auth_token">TOKEN</string>,
- # 输出形如 flutter.auth_token">TOKEN,Python 侧再切出 TOKEN
- REMOTE_GREP_CMD = f"grep -o 'flutter.auth_token\">[^<]*' {PREFS_PATH}"
- # grep 输出里 token 前的分隔标记(值紧跟在 "> 之后)
- _VALUE_SEP = '">'
- def _run(cmd: list[str], timeout: int = 20) -> subprocess.CompletedProcess:
- """执行一条命令并返回结果对象。
- Args:
- cmd (list[str]): 完整命令 argv 列表,来自各调用处拼好的 adb 命令。
- timeout (int, optional): 超时秒数。Defaults to 20。
- Returns:
- subprocess.CompletedProcess: 执行结果,含 returncode / stdout / stderr。
- Raises:
- RuntimeError: 未找到 adb 可执行文件,或命令超时时抛出。
- """
- try:
- return subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
- except FileNotFoundError as e:
- # 环境里没有 adb 可执行文件(未装 platform-tools 或未加入 PATH)
- raise RuntimeError(
- f"[{sys._getframe().f_code.co_name}] 未找到 adb,请确认已把 platform-tools 加入 PATH"
- ) from e
- except subprocess.TimeoutExpired as e:
- raise RuntimeError(
- f"[{sys._getframe().f_code.co_name}] adb 命令超时,请检查模拟器与 adb 连接"
- ) from e
- def _prepare_device() -> None:
- """确保已连上 MuMu 并让 adbd 以 root 运行。
- 两步都幂等:已连接会提示 already connected,已 root 会提示 already running as
- root,均不影响后续读取。连接 / 提权若失败,不在此中断,交由读取阶段统一暴露。
- Returns:
- None: 无返回值。
- """
- # 幂等连接:未连则建立,已连直接复用
- _run(["adb", "connect", MUMU_ADB_ADDR])
- # 幂等提权:MuMu 支持 adb root,读 App 私有目录需要 root 身份
- _run(["adb", "-s", MUMU_ADB_ADDR, "root"])
- def get_token() -> str:
- """从已登录的卡玩 App 抓取当前登录态 token。
- 先确保连上 MuMu 且 adbd 为 root,再以 root 读取 Flutter 的
- SharedPreferences,grep 出 flutter.auth_token 的值。整条远程命令作为单个
- 字符串传给 adb shell,保证单引号在设备端正确解析。
- Returns:
- str: 当前有效的裸 token(Sa-Token 风格 JWT,不含任何前缀)。
- Raises:
- RuntimeError: 设备未连接 / 未 root / App 未登录 / 未抓到 token 时抛出。
- """
- _prepare_device()
- # 整条远程命令作为单个字符串传参,避免 > 与 [^<]* 被本机 shell 或设备 shell 误解析
- result = _run(["adb", "-s", MUMU_ADB_ADDR, "shell", REMOTE_GREP_CMD])
- if result.returncode != 0:
- # grep 未匹配(App 未登录 / 文件不存在)也会返回非 0,统一当作失败处理
- err = result.stderr.strip() or result.stdout.strip() or "未知错误"
- raise RuntimeError(
- f"[{sys._getframe().f_code.co_name}] 抓取失败:{err}\n"
- f"排查:1) 模拟器是否开着且 adb 能连上 {MUMU_ADB_ADDR} "
- f"2) adb 是否已 root 3) 卡玩 App 是否已登录"
- )
- # 输出形如 flutter.auth_token">eyJ...,切出 "> 之后的 token(.strip 顺带去掉 \r)
- token = ""
- for line in result.stdout.splitlines():
- line = line.strip()
- if _VALUE_SEP in line:
- token = line.split(_VALUE_SEP, 1)[1].strip()
- if token:
- break
- if not token:
- # 匹配到字段但值为空 = App 已退出登录 / 尚未登录
- raise RuntimeError(
- f"[{sys._getframe().f_code.co_name}] 未抓到 token,请确认卡玩 App 已登录并稳定运行几秒后再试"
- )
- return token
- if __name__ == "__main__":
- try:
- print(get_token())
- except RuntimeError as exc:
- print(exc)
- sys.exit(1)
|