114 lines
3.7 KiB
Python
114 lines
3.7 KiB
Python
"""登录助手 — 解析浏览器导出的 cookies JSON,保存为 session.json。
|
||
|
||
Cookie 白名单基于 HAR 抓包分析:
|
||
- 必需 cookie 见 REQUIRED_COOKIE_NAMES,可选 cookie 见 OPTIONAL_COOKIE_NAMES
|
||
- JSESSIONID 仅 portal 主页面需要(加载 pickup 页面时作为 Referer)
|
||
- enable_HFZrs6pXKulz 仅 portal UI 配置,pickup-web 不需要
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
import os
|
||
import sys
|
||
|
||
# 基于 portal-to-pickup-full-flow.har 分析得出的必需 cookie
|
||
# 导入及会话检查要求以下 9 个 cookie 齐全
|
||
REQUIRED_COOKIE_NAMES: set[str] = {
|
||
# 会话认证(核心 — 缺失则 401/重定向到登录页)
|
||
"jdpt.session.id",
|
||
# 组织层级(固定值,session 生命周期内不变,缺失则权限异常)
|
||
"CORAL_GRAYLEVEL",
|
||
"PROV_GRAYLEVEL",
|
||
"CITY_GRAYLEVEL",
|
||
"COUNTY_GRAYLEVEL",
|
||
"ORG_GRAYLEVEL",
|
||
# $_ts 安全框架 token(缺失则 c93l5Fya 校验失败,接口拒绝服务)
|
||
"HFZrs6pXKulzO", # 组织级 token,固定不变
|
||
"HFZrs6pXKulzP", # 请求级动态 token,服务端 Set-Cookie 持续更新
|
||
# portal 主页面会话(缺失则无法加载 pickup 页面,fetch_page_resources 失败)
|
||
"JSESSIONID",
|
||
}
|
||
|
||
OPTIONAL_COOKIE_NAMES: set[str] = {"HFZrs6pXKulzS"}
|
||
ALLOWED_COOKIE_NAMES: set[str] = REQUIRED_COOKIE_NAMES | OPTIONAL_COOKIE_NAMES
|
||
|
||
|
||
def _is_cookie_allowed(name: str) -> bool:
|
||
"""判断 cookie 是否在导入和保存的白名单中。"""
|
||
return name in ALLOWED_COOKIE_NAMES
|
||
|
||
|
||
def get_session_path() -> str:
|
||
if getattr(sys, "frozen", False):
|
||
base = os.path.dirname(sys.executable)
|
||
else:
|
||
base = os.path.dirname(os.path.abspath(__file__))
|
||
return os.path.join(base, "session.json")
|
||
|
||
|
||
def parse_json_and_save(json_text: str) -> int:
|
||
"""解析浏览器导出的 cookies JSON 数组,过滤并保存白名单 cookie。
|
||
|
||
只保存 ALLOWED_COOKIE_NAMES 中的 cookie,仅检查 REQUIRED_COOKIE_NAMES 是否齐全。
|
||
返回保存的 cookie 数量。
|
||
"""
|
||
raw = json.loads(json_text)
|
||
if not isinstance(raw, list):
|
||
raise ValueError("JSON 必须是数组格式")
|
||
|
||
# 过滤目标域名 + 白名单 cookie 名称
|
||
target = "10.4.188.1"
|
||
cookies = []
|
||
seen = set()
|
||
skipped = []
|
||
for c in raw:
|
||
domain = c.get("domain", "")
|
||
if target not in domain:
|
||
continue
|
||
name = c.get("name", "").strip()
|
||
path = c.get("path", "/")
|
||
if not name:
|
||
continue
|
||
if not _is_cookie_allowed(name):
|
||
skipped.append(name)
|
||
continue
|
||
key = (name, domain, path)
|
||
if key in seen:
|
||
continue
|
||
seen.add(key)
|
||
cookies.append({
|
||
"name": name,
|
||
"value": c.get("value", ""),
|
||
"domain": domain,
|
||
"path": path,
|
||
})
|
||
|
||
# 检查必需 cookie 是否齐全(全部 9 个缺一不可)
|
||
found_names = {c["name"] for c in cookies}
|
||
missing = REQUIRED_COOKIE_NAMES - found_names
|
||
if missing:
|
||
raise ValueError(
|
||
f"缺少必需 cookie ({len(missing)} 个): {', '.join(sorted(missing))}"
|
||
)
|
||
|
||
data = {
|
||
"cookies": cookies,
|
||
"saved_from": f"https://{target}",
|
||
"filtered_count": len(skipped),
|
||
}
|
||
session_path = get_session_path()
|
||
os.makedirs(os.path.dirname(session_path) or ".", exist_ok=True)
|
||
with open(session_path, "w", encoding="utf-8") as f:
|
||
json.dump(data, f, ensure_ascii=False, indent=2)
|
||
return len(cookies)
|
||
|
||
|
||
def load_session_info() -> dict | None:
|
||
"""读取已保存的 session 信息用于 UI 展示。"""
|
||
try:
|
||
with open(get_session_path(), "r") as f:
|
||
return json.load(f)
|
||
except (FileNotFoundError, json.JSONDecodeError):
|
||
return None
|