diff --git a/docs/login-command-plan.md b/docs/login-command-plan.md index 3a8c452..a30397e 100644 --- a/docs/login-command-plan.md +++ b/docs/login-command-plan.md @@ -3,6 +3,11 @@ 状态:待用户确认,未实施。 关联待办:`todos/open-td-20260921-a1b2c3-lionwebsite-login-tg.md`。 +本方案**对齐 PersonalHub 已有的面板登录实现**(`git.lionwebsite.xyz/lion/PersonalService`, +`personal_service/app.py` + `personal_service/security.py`)。PersonalHub 已经用同一套 +机制跑在生产上,机器人命令 `/login`、一次性票据、Cookie 会话、退出端点都齐全,因此 +LionWebsite 侧直接照搬即可,不需要发明新协议。 + ## 1. 现状 - 个人面板入口是 `https://personal.lionwebsite.xyz/index`(桌面)与 `/mobile`(移动)。 @@ -15,49 +20,74 @@ liondown / bigcat)。它是独立的固定字面量:不轮换、不区分人,且随 JS 产物公开。 - 存储节点另有两处写死 `alone`:`storageNode` 的 `CustomUtil.java`(向 `/message2me` 推送)与 `MultiThreadedHTTPServer.java`(本机 HTTP 鉴权)。 -- 机器人(QQ `LionQQBot` + Telegram `PersonalHub`,主机 vm103)目前没有 `/login`。 -## 2. 目标 +## 2. 对照:PersonalHub 现有实现 -1. 机器人在 QQ 与 Telegram 都能执行 `/login`,返回一条可点击的个人面板登录地址。 -2. 地址自带短期凭证,点开即登录,不需要手输授权码。 -3. 长期固定的 `alone` 不再出现在前端产物与聊天消息里。 -4. 凭证可过期、可吊销,登录行为在后端留痕。 +| 关键点 | PersonalHub 做法 | LionWebsite 对应做法 | +| --- | --- | --- | +| 票据 | `OneTimePanelLoginTickets`:进程内字典,`secrets.token_urlsafe(32)`,TTL 300 秒,`consume` 即弹出,最多 8 张活跃 | 同样进程内一次性票据,参数可配 | +| 存法 | 只存 SHA-256 摘要,不存明文 | 相同 | +| 链接 | `panel_login_link()` 只允许配置里的固定 HTTPS origin,附加 `?token=` | `https://personal.lionwebsite.xyz/login?t=` | +| 落地点 | `GET /panel/telegram-login` 校验票据 → 写 session → 303 跳 `/panel` | `GET /login` 校验票据 → 写会话 → 302 跳 `/index` | +| 会话 | Starlette `SessionMiddleware`,签名 Cookie,`same_site=strict`,14 天 | Spring Boot `HttpSession` + Tomcat Cookie,或自签 Cookie | +| 口令登录 | 另有一条 `/api/panel/login`:口令 + TOTP,带 `AttemptLimiter(5, 300)` | 可选,见下文第 4 节 | +| 退出 | `POST /api/panel/logout` 清 session | `GET /login/logout` | +| 机器人 | `TELEGRAM_COMMANDS` 加 `login`,`telegram.py` 派发到 `telegram_panel_login()` | QQ / TG 各加 `/login` | +| 未配置时 | 返回「个人面板尚未完整配置,无法生成登录链接。」 | 返回同类中文提示,不抛异常 | -## 3. 推荐方案:短期签名链接 + 会话 Cookie +关键差异:PersonalHub 是 ASGI,直接有 `request.session`;LionWebsite 是 Spring Boot, +需要自己选会话载体。推荐直接用 Spring 的 `HttpSession`,由容器签发 `JSESSIONID` +Cookie,省去自签实现;PersonalHub 的签名 Cookie 是为了在 ASGI 侧无需额外依赖。 -### 3.1 后端(`lionwebsite-backend`,主机 us9929) +## 3. 推荐方案:一次性票据换会话 -1. 新增登录端点。nginx 无需改动:`location /` 已把请求改写成 `/personal/...`, - 因此 `https://personal.lionwebsite.xyz/login?t=...` 会落到后端 `/personal/login`。 - - `GET /login?t=`:校验票据 → 下发 HttpOnly Cookie(`personal_session`, - `SameSite=Lax`、`Secure`,有效期建议 30 天)→ 302 跳 `/index`。 - - 票据格式建议 `v1.<签发时间戳>.`,有效期 5 分钟。 -2. `PersonalInterceptor` 的放行条件改为「有效会话 Cookie **或** 合法 `AuthCode`」。 - 过渡期保留 `AuthCode`,避免影响下载器前端与存储节点推送。 -3. `InterceptorConfiguration` 必须排除 `/personal/login`,否则登录端点会被自己拦住。 -4. 共享密钥通过 `application.yaml` 的环境变量覆盖位注入(如 `personal.login-secret`), - 密钥值不进仓库、不进日志。 -5. 可选 `GET /login/logout` 清除 Cookie。 +沿用 PersonalHub 的形状,票据一次性、短时,会话 Cookie 长期。 -选签名票据而不是「后端签发一次性 token」的理由:机器人与后端之间不需要新增网络调用 -和状态存储,双方共享一个密钥即可。代价是票据在 5 分钟窗口内可重放,对私聊场景可接受; -若要求真正一次性,改为后端 mint 端点 + 内存 token 表。 +### 3.1 后端(`lionwebsite-backend`,host-us9929) -### 3.2 机器人(QQ + Telegram,主机 vm103) +1. 新增 `PanelLoginTickets`:进程内 `ConcurrentHashMap`,值为过期时间戳;签发 + `SecureRandom` 32 字节 base64url;校验时**先移除再判断**,保证一次性;同时清理过期项 + 并限制活跃上限(防内存增长)。 +2. 新增登录端点。nginx 无需改动:`location /` 已把请求改写成 `/personal/...`,因此 + `https://personal.lionwebsite.xyz/login?t=...` 会落到后端 `/personal/login`。 + - `GET /login?t=`:命中票据 → 建立会话(`session.setAttribute("personalAuthenticated", true)`) + → 302 跳 `/index`;未命中或被复用 → 302 跳 `/personal/denied`(或回登录提示页)。 + - `GET /login/logout`:`session.invalidate()` → 跳回提示页。 +3. `PersonalInterceptor` 放行条件改为「有效会话 **或** 合法 `AuthCode`」。过渡期保留 + `AuthCode`,避免影响下载器前端与存储节点推送。 +4. `InterceptorConfiguration` 必须把 `/personal/login`、`/personal/login/logout` 排除在 + `PersonalInterceptor` 之外,否则登录端点会被自己拦住。 +5. 票据 TTL 与上限走 `application.yaml` 配置项,便于调整;不涉及密钥,无需秘密库。 +6. 登录失败写日志(时间、来源 IP、结果),成功也记一行,便于回溯谁在何时登录。 -- QQ:在 LionQQBot 插件里新增 `/login`(别名 `/登录`),复用既有命令注册与主人校验。 -- Telegram:`sync_commands` 菜单增加 `/login`,`telegram.py` 派发到同名处理函数。 -- 处理函数:用共享密钥生成票据,回复 - `https://personal.lionwebsite.xyz/login?t=`,并附一句有效期提示。 -- 密钥存 PersonalHub 秘密库,共享盘只保留 `secret://` 引用。 -- 按现行约定,新增命令必须同时覆盖 QQ 与 Telegram,帮助菜单同步更新。 +### 3.2 机器人(QQ + Telegram,host-vm103-debian-qq) + +PersonalHub 已经具备生成链接的全部能力,但**它签的是自己的面板票据**。给 LionWebsite +用有两种接法: + +- 接法 A(推荐,改动最小):PersonalHub 复用已有的 `_login_handler` 形状,新增一个 + 「生成 LionWebsite 登录链接」的处理函数。它需要拿到一张 LionWebsite 票据,因此 + LionWebsite 侧要提供一个**受保护的签发端点**,例如 `POST /personal/login/ticket`, + 用现有共享盘内部令牌或固定内部密钥鉴权,返回一次性票据;PersonalHub 调用后拼成链接。 +- 接法 B(无新增网络调用):两端约定一个共享密钥,PersonalHub 本地用 + `HMAC-SHA256(密钥, 时间戳)` 自签票据,LionWebsite 校验签名与时间窗。省一次调用, + 但票据在时间窗内可重放,需要额外一次性状态才能封住;而 PersonalHub 现有 + `OneTimePanelLoginTickets` 本来就是进程内一次性表,接法 A 更贴近既有做法。 + +两种接法下,QQ 与 TG 都走同一个处理函数,`TELEGRAM_COMMANDS` 加 `login`, +QQ 侧加 `/login`(别名 `/登录`),回复文案沿用 PersonalHub 的 +「已生成一次性登录链接,X 分钟内有效且只能使用一次……请勿转发」。 + +跨主机连通性:us9929 与 vm103 之间已有 EasyTier(us9929 侧 `10.0.0.6`, +vm103 即 `192.168.0.204`),且 `personal.lionwebsite.xyz` 直接解析到 us9929 公网 IP +`38.60.92.138`,不经过 Cloudflare。因此接法 A 的内网调用与浏览器打开链接都不需要 +额外放通。 ### 3.3 前端(`PrivateMain` 桌面 / `PrivateMainForMobile` 移动) - 删除 `authCode: "alone"` 常量与 `?AuthCode=` 查询串,改为依赖同源 Cookie (axios 同源请求默认携带 Cookie)。 -- 未登录或被拒时显示提示页「登录已过期,请在机器人里发送 /login」,不要静默失败。 +- 未登录或会话过期时显示提示页「登录已过期,请在机器人里发送 /login」,不要静默失败。 - 下载器前端(`lionwebsite-frontend-desktop` / `-mobile`)使用每个用户自己的授权码, 本次不动。 @@ -67,23 +97,27 @@ - 存储节点两处 `alone` 换成配置项,属 storageNode 仓库,单独一次发布。 - 轮换后确认无调用方仍依赖旧字面量。 -## 4. 更小的备选 +## 4. 备选与取舍 -- 方案 B(最小改动):前端增加「从 URL 读取 `AuthCode` 并记住」的逻辑,机器人 `/login` - 直接回 `https://personal.lionwebsite.xyz/index?AuthCode=alone`。半天内可上线,但长期 - 密钥仍会进入聊天记录、浏览器历史与 nginx 日志,也没有解决 `alone` 写死的问题。 -- 方案 C(只治理配置):把 `alone` 从代码搬到配置并轮换,机器人从配置读取。安全提升有限。 +- 方案 B(最小改动):前端增加「从 URL 读取 `AuthCode` 并记住」,机器人 `/login` 直接回 + `https://personal.lionwebsite.xyz/index?AuthCode=alone`。半天可上线,但固定密钥仍会进 + 聊天记录、浏览器历史与 nginx 日志,也没解决 `alone` 写死的问题。 +- 方案 C(只治理配置):把 `alone` 从代码搬到配置并轮换,安全提升有限。 +- 口令 + TOTP 登录页:PersonalHub 有这一套,但用户明确要的是「输入 /login 就弹出登录 + 地址」,口令 TOTP 属于另一条路径。如果以后想在电脑上直接登录而不经机器人,可以再补, + 两者共用同一个会话状态即可。 ## 5. 需要确认的三点 -1. 采用推荐方案(Cookie 会话),还是先上备选 B? +1. 采用推荐方案(一次性票据 + 会话),还是先上备选 B? 2. 登录链接只允许主人使用,还是允许机器人给其他授权用户分别签发(对应 `User` 表账号)? -3. 会话有效期:建议 30 天滑动过期,可调整。 +3. 会话有效期:PersonalHub 是 14 天,建议 LionWebsite 也用 14 天滑动过期,可调整。 ## 6. 实施顺序 -1. 后端票据签发与 Cookie 校验、拦截器放行,含单元测试。 -2. 前端去掉 `alone`,补未登录提示。 -3. 机器人 QQ 与 Telegram 双向 `/login`。 -4. 存储节点两处 `alone` 改为配置。 -5. 退役字面量 `alone`。 +1. 后端票据签发与会话校验、拦截器放行、退出端点,含单元测试。 +2. 后端受保护的票据签发端点(接法 A)或共享密钥签名(接法 B)。 +3. 前端去掉 `alone`,补未登录提示。 +4. 机器人 QQ 与 Telegram 双向 `/login`。 +5. 存储节点两处 `alone` 改为配置。 +6. 退役字面量 `alone`。