get_token.py 5.1 KB

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