新增功能:
- 登录成功后凭据加密落盘(auth.json,DPAPI 同 nodes.json 保护级别) - 每次启动后台用已保存凭据拉取最新节点配置(内容比对,变化才覆盖):服务端修改 nodes.json 后,客户端下次启动自动同步 - 凭据失效(401)或网络失败时保留本地配置不破坏;登录框在保存过凭据时自动预填,重新登录一键完成
This commit is contained in:
@@ -663,6 +663,55 @@ def _nodes_decrypt(blob: bytes) -> str:
|
||||
except Exception:
|
||||
return blob.decode("utf-8")
|
||||
|
||||
def _auth_save(username, password):
|
||||
"""登录凭据加密保存到用户数据目录(DPAPI,本机本用户可解)。"""
|
||||
import json
|
||||
text = json.dumps({"username": username, "password": password}, ensure_ascii=False)
|
||||
_USER_DIR.mkdir(parents=True, exist_ok=True)
|
||||
(_USER_DIR / "auth.json").write_bytes(_nodes_encrypt(text))
|
||||
|
||||
def _auth_load():
|
||||
"""读取已保存的登录凭据;不存在或损坏返回 None。"""
|
||||
try:
|
||||
import json
|
||||
f = _USER_DIR / "auth.json"
|
||||
if not f.is_file():
|
||||
return None
|
||||
data = json.loads(_nodes_decrypt(f.read_bytes()))
|
||||
if not (isinstance(data, dict) and data.get("username") and data.get("password")):
|
||||
return None
|
||||
return data
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def _nodes_sync_remote():
|
||||
"""启动时用已保存凭据拉取最新节点配置(内容比对,变化才覆盖)。"""
|
||||
cred = _auth_load()
|
||||
if not cred:
|
||||
return # 从未登录过,等登录框处理
|
||||
url = str(_config_val("nodes_url", "") or "").strip()
|
||||
if not url:
|
||||
return
|
||||
try:
|
||||
import json
|
||||
data = _fetch_nodes_auth(url, cred["username"], cred["password"])
|
||||
if not (isinstance(data, list) and data
|
||||
and all(isinstance(n, dict) for n in data)
|
||||
and str(data[0].get("domain") or "").strip()):
|
||||
raise ValueError("返回的节点数据无效")
|
||||
text = json.dumps(data, ensure_ascii=False, indent=2) + "\n"
|
||||
local = _USER_DIR / "nodes.json"
|
||||
if local.is_file() and _nodes_decrypt(local.read_bytes()) == text:
|
||||
log("节点配置已是最新")
|
||||
return
|
||||
_USER_DIR.mkdir(parents=True, exist_ok=True)
|
||||
local.write_bytes(_nodes_encrypt(text))
|
||||
global _NODES
|
||||
_NODES = None # 使后续读取(节点菜单/开启代理)拿到新配置
|
||||
log(f"节点配置已更新: {len(data)} 个节点 (nodes.json)")
|
||||
except Exception as e:
|
||||
log(f"节点配置在线更新失败,使用本地配置: {e}")
|
||||
|
||||
def _fetch_nodes_auth(url, user, password, timeout=10):
|
||||
"""带 Basic 认证拉取 nodes.json(参与 nginx 认证;直连,不经过系统代理)。"""
|
||||
import json
|
||||
@@ -1182,6 +1231,12 @@ class LoginDialog(QDialog):
|
||||
self.ed_pass.returnPressed.connect(self._submit)
|
||||
form.addWidget(self.ed_pass)
|
||||
|
||||
# 保存过凭据时预填,重新登录只需点一下
|
||||
cred = _auth_load()
|
||||
if cred:
|
||||
self.ed_user.setText(cred["username"])
|
||||
self.ed_pass.setText(cred["password"])
|
||||
|
||||
self.lb_err = QLabel("")
|
||||
self.lb_err.setStyleSheet(f"color: {C_RED}; font-size: 11px;")
|
||||
self.lb_err.setFixedHeight(14)
|
||||
@@ -1237,6 +1292,7 @@ class LoginDialog(QDialog):
|
||||
text = json.dumps(data, ensure_ascii=False, indent=2) + "\n"
|
||||
_USER_DIR.mkdir(parents=True, exist_ok=True)
|
||||
(_USER_DIR / "nodes.json").write_bytes(_nodes_encrypt(text))
|
||||
_auth_save(user, password) # 记住凭据,之后每次启动自动同步节点
|
||||
self._done.emit(True, str(len(data)))
|
||||
except Exception as e:
|
||||
msg = ("认证失败,请检查用户名/密码" if getattr(e, "code", None) == 401
|
||||
@@ -1325,6 +1381,8 @@ class MainWindow(QWidget):
|
||||
threading.Thread(target=self._check_update, daemon=True).start()
|
||||
# 后台检查 PAC 数据更新(首次启动自动拉取到用户数据目录)
|
||||
threading.Thread(target=_user_init_remote, daemon=True).start()
|
||||
# 后台用已保存凭据同步最新节点配置(服务端改动下次启动生效)
|
||||
threading.Thread(target=_nodes_sync_remote, daemon=True).start()
|
||||
# 节点未配置时延迟弹出登录框(等窗口显示后)
|
||||
QTimer.singleShot(600, self._maybe_show_login)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user