修复: - 节点认证方式升级为 SSO:user/password 按 URL-safe Base64 作为查询参数拼接在 config.json 的 nodes_url 后(旧 nginx auth_basic 已废弃),服务端返回的 401/403/429 等中文原因直接透出到登录框,登录框输入占位改为 SSO 邮箱

This commit is contained in:
RoyirinRosmontis
2026-09-03 22:40:50 +08:00
parent f8eba465a9
commit 17cdb276ea
+40 -14
View File
@@ -895,16 +895,40 @@ def _nodes_sync_remote():
except Exception as e:
log(f"节点配置在线更新失败,使用本地配置: {e}")
class _AuthError(Exception):
"""订阅接口返回的错误:code 为 HTTP 状态码,str 为服务端返回的中文提示。"""
def __init__(self, msg, code=0):
super().__init__(msg)
self.code = code
def _b64url(s: str) -> str:
"""SSO 认证查询参数的 URL-safe Base64 编码(末尾 padding 省略)。"""
return base64.urlsafe_b64encode(str(s).encode("utf-8")).decode("ascii").rstrip("=")
def _fetch_nodes_auth(url, user, password, timeout=10):
"""带 Basic 认证拉取 nodes.json(参与 nginx 认证;直连,不经过系统代理)。"""
"""用 SSO 账号拉取 nodes.json:user/password 按 URL-safe Base64 作为查询参数
拼接在 nodes_url 后(旧版 nginx auth_basic 已废弃);直连,不经过系统代理。
失败抛 _AuthError(服务端拒绝,str 为服务端中文原因)或 urllib 异常(网络问题)。"""
import json
import urllib.request
import urllib.error
opener = urllib.request.build_opener(urllib.request.ProxyHandler({}))
req = urllib.request.Request(url)
req.add_header("Authorization", "Basic " + base64.b64encode(
f"{user}:{password}".encode()).decode())
with opener.open(req, timeout=timeout) as r:
return json.loads(r.read().decode("utf-8"))
sep = "&" if "?" in url else "?"
req_url = f"{url}{sep}user={_b64url(user)}&password={_b64url(password)}"
try:
with opener.open(req_url, timeout=timeout) as r:
return json.loads(r.read().decode("utf-8"))
except urllib.error.HTTPError as e:
# 401/403/429/5xx:服务端 body 是面向用户的中文原因,直接透出
body = ""
try:
body = e.read().decode("utf-8", "replace").strip()
except Exception:
pass
raise _AuthError(body or f"请求被拒绝 (HTTP {e.code})", e.code) from None
def _user_init_local():
@@ -922,7 +946,7 @@ def _user_init_local():
def _user_init_remote(base_url=None):
"""检查并更新 PAC 数据(后台线程):
直接拉取两个数据文件(内含 version/date),按版本比对后更新;失败用本地副本。
节点配置的拉取走登录框(nodes_url 需 nginx 认证,见 LoginDialog)。
节点配置的拉取走登录框(nodes_url 需 SSO 认证,见 LoginDialog)。
"""
if _skip_update_check():
return # 设置跳过:不拉取也不打日志
@@ -1397,7 +1421,8 @@ class NodeLabel(QLabel):
# ── 节点配置登录框 ─────────────────────────────────────
class LoginDialog(QDialog):
"""节点配置登录框:凭据参与 nodes_url 的 nginx 认证,成功后拉取并加密保存 nodes.json。"""
"""节点配置登录框:SSO 账号(邮箱+密码,base64url 查询参数)认证 nodes_url,
成功后拉取并加密保存 nodes.json。"""
_done = pyqtSignal(bool, str)
def __init__(self, parent=None):
@@ -1454,7 +1479,7 @@ class LoginDialog(QDialog):
form = QVBoxLayout()
form.setSpacing(6)
self.ed_user = QLineEdit()
self.ed_user.setPlaceholderText("登录名")
self.ed_user.setPlaceholderText("SSO 邮箱")
form.addWidget(self.ed_user)
self.ed_pass = QLineEdit()
@@ -1509,7 +1534,7 @@ class LoginDialog(QDialog):
threading.Thread(target=self._worker, args=(user, password), daemon=True).start()
def _worker(self, user, password):
"""后台登录:Basic 认证拉取 nodes.json,校验后加密写入用户数据目录。"""
"""后台登录:SSO 认证拉取 nodes.json,校验后加密写入用户数据目录。"""
try:
import json
url = str(_config_val("nodes_url", "") or "").strip()
@@ -1523,12 +1548,13 @@ 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) # 记住凭据,之后每次启动自动同步节点
_auth_save(user, password) # 记住 SSO 凭据,之后每次启动自动同步节点
self._done.emit(True, str(len(data)))
except _AuthError as e:
# 服务端拒绝:直接展示其中文原因(认证失败/未开通/限流等)
self._done.emit(False, str(e))
except Exception as e:
msg = ("认证失败,请检查用户名/密码" if getattr(e, "code", None) == 401
else f"登录失败: {e}")
self._done.emit(False, msg)
self._done.emit(False, f"登录失败: {e}")
def _on_done(self, ok, info):
self.btn_ok.setEnabled(True)