Files
lionwebsite-backend/docs/login-command-plan.md
T

124 lines
8.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# LionWebsite 一键登录指令方案(/login)
状态:待用户确认,未实施。
关联待办:`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`(移动)。
nginx 把 `/` 代理到后端 `/personal/`,把 `/index`、`/mobile` 映射到静态入口文件。
- 面板没有登录页。`PrivateMain` 与 `PrivateMainForMobile` 的 `src/store/index.js`
把 `authCode: "alone"` 写死,所有请求都带 `?AuthCode=alone`。
- 后端 `PersonalInterceptor` 对 `/personal/**`、`/remote/**` 直接比较字面量 `alone`;
`AdaptorFilter` 也用它决定移动端跳转。`/remote/**` 目前没有对应控制器,属历史遗留。
- `alone` 不在 `User` 表里(表内是 big lion / pubraseer / temp / au283602 / 0619 /
liondown / bigcat)。它是独立的固定字面量:不轮换、不区分人,且随 JS 产物公开。
- 存储节点另有两处写死 `alone`:`storageNode` 的 `CustomUtil.java`(向 `/message2me`
推送)与 `MultiThreadedHTTPServer.java`(本机 HTTP 鉴权)。
## 2. 对照:PersonalHub 现有实现
| 关键点 | 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` |
| 未配置时 | 返回「个人面板尚未完整配置,无法生成登录链接。」 | 返回同类中文提示,不抛异常 |
关键差异:PersonalHub 是 ASGI,直接有 `request.session`;LionWebsite 是 Spring Boot,
需要自己选会话载体。推荐直接用 Spring 的 `HttpSession`,由容器签发 `JSESSIONID`
Cookie,省去自签实现;PersonalHub 的签名 Cookie 是为了在 ASGI 侧无需额外依赖。
## 3. 推荐方案:一次性票据换会话
沿用 PersonalHub 的形状,票据一次性、短时,会话 Cookie 长期。
### 3.1 后端(`lionwebsite-backend`,host-us9929)
1. 新增 `PanelLoginTickets`:进程内 `ConcurrentHashMap`,值为过期时间戳;签发
`SecureRandom` 32 字节 base64url;校验时**先移除再判断**,保证一次性;同时清理过期项
并限制活跃上限(防内存增长)。
2. 新增登录端点。nginx 无需改动:`location /` 已把请求改写成 `/personal/...`,因此
`https://personal.lionwebsite.xyz/login?t=...` 会落到后端 `/personal/login`。
- `GET /login?t=<ticket>`:命中票据 → 建立会话(`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、结果),成功也记一行,便于回溯谁在何时登录。
### 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」,不要静默失败。
- 下载器前端(`lionwebsite-frontend-desktop` / `-mobile`)使用每个用户自己的授权码,
本次不动。
### 3.4 收尾:退役 `alone`
- `PersonalInterceptor`、`AdaptorFilter` 不再比较字面量;移动端跳转改为原样透传查询串。
- 存储节点两处 `alone` 换成配置项,属 storageNode 仓库,单独一次发布。
- 轮换后确认无调用方仍依赖旧字面量。
## 4. 备选与取舍
- 方案 B(最小改动):前端增加「从 URL 读取 `AuthCode` 并记住」,机器人 `/login` 直接回
`https://personal.lionwebsite.xyz/index?AuthCode=alone`。半天可上线,但固定密钥仍会进
聊天记录、浏览器历史与 nginx 日志,也没解决 `alone` 写死的问题。
- 方案 C(只治理配置):把 `alone` 从代码搬到配置并轮换,安全提升有限。
- 口令 + TOTP 登录页:PersonalHub 有这一套,但用户明确要的是「输入 /login 就弹出登录
地址」,口令 TOTP 属于另一条路径。如果以后想在电脑上直接登录而不经机器人,可以再补,
两者共用同一个会话状态即可。
## 5. 需要确认的三点
1. 采用推荐方案(一次性票据 + 会话),还是先上备选 B?
2. 登录链接只允许主人使用,还是允许机器人给其他授权用户分别签发(对应 `User` 表账号)?
3. 会话有效期:PersonalHub 是 14 天,建议 LionWebsite 也用 14 天滑动过期,可调整。
## 6. 实施顺序
1. 后端票据签发与会话校验、拦截器放行、退出端点,含单元测试。
2. 后端受保护的票据签发端点(接法 A)或共享密钥签名(接法 B)。
3. 前端去掉 `alone`,补未登录提示。
4. 机器人 QQ 与 Telegram 双向 `/login`。
5. 存储节点两处 `alone` 改为配置。
6. 退役字面量 `alone`。