登录方案文档更新为已实施状态,补机器人侧票据规格
This commit is contained in:
+76
-96
@@ -1,123 +1,103 @@
|
||||
# LionWebsite 一键登录指令方案(/login)
|
||||
# LionWebsite 一键登录(/login):实现说明与机器人侧待办
|
||||
|
||||
状态:待用户确认,未实施。
|
||||
状态:**主站侧已实施上线**(提交 `00e2715`);机器人侧(QQ / Telegram)**待实施**。
|
||||
关联待办:`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 侧直接照搬即可,不需要发明新协议。
|
||||
用户已明确选择**接法 B:共享密钥自签票据**,理由是机器人到主站的网络调用不可靠。
|
||||
因此主站**不提供**任何票据签发端点,也不需要机器人侧发起 HTTP 请求——机器人本地
|
||||
用共享密钥算出票据即可。
|
||||
|
||||
## 1. 现状
|
||||
## 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 鉴权)。
|
||||
### 1.1 票据格式
|
||||
|
||||
## 2. 对照:PersonalHub 现有实现
|
||||
```
|
||||
v1.<签发时间的 Unix 秒级时间戳>.<HMAC-SHA256 十六进制小写>
|
||||
```
|
||||
|
||||
| 关键点 | 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` |
|
||||
| 未配置时 | 返回「个人面板尚未完整配置,无法生成登录链接。」 | 返回同类中文提示,不抛异常 |
|
||||
签名原文为 `v1.<时间戳>`(固定前缀 `v1.` 加时间戳本身),密钥为共享密钥的 UTF-8 字节。
|
||||
等价于 `hex(HMAC_SHA256(key, "v1." + timestamp))`。
|
||||
|
||||
关键差异:PersonalHub 是 ASGI,直接有 `request.session`;LionWebsite 是 Spring Boot,
|
||||
需要自己选会话载体。推荐直接用 Spring 的 `HttpSession`,由容器签发 `JSESSIONID`
|
||||
Cookie,省去自签实现;PersonalHub 的签名 Cookie 是为了在 ASGI 侧无需额外依赖。
|
||||
### 1.2 校验规则
|
||||
|
||||
## 3. 推荐方案:一次性票据换会话
|
||||
- 必须恰好三段,第一段必须是 `v1`;
|
||||
- 签名比较用常量时间(`MessageDigest.isEqual`);
|
||||
- 时间戳只接受 `[now - 300s, now]`,**未来时间戳一律拒绝**(防伪造者用远期时间换长期有效);
|
||||
- 窗口内**允许重放**(用户明确接受),过期即失效;
|
||||
- 默认 300 秒有效期,可用 `personal.login.ticket-ttl-seconds` 调整,下限 30 秒。
|
||||
|
||||
沿用 PersonalHub 的形状,票据一次性、短时,会话 Cookie 长期。
|
||||
### 1.3 端点
|
||||
|
||||
### 3.1 后端(`lionwebsite-backend`,host-us9929)
|
||||
| 端点 | 作用 |
|
||||
| --- | --- |
|
||||
| `GET /login?t=<票据>` | 校验票据 → 作废旧会话(防会话固定)→ 建新会话 → 302 跳 `/index`;失败 302 跳 `/denied` |
|
||||
| `GET /login/logout` | 销毁会话 → 302 跳 `/denied` |
|
||||
| `GET /denied` | 静态提示页:「请在机器人里发送 `/login`」 |
|
||||
|
||||
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、结果),成功也记一行,便于回溯谁在何时登录。
|
||||
nginx 无需改动:`location /` 会把 `/login` 改写成后端 `/personal/login`。
|
||||
|
||||
### 3.2 机器人(QQ + Telegram,host-vm103-debian-qq)
|
||||
### 1.4 会话与鉴权
|
||||
|
||||
PersonalHub 已经具备生成链接的全部能力,但**它签的是自己的面板票据**。给 LionWebsite
|
||||
用有两种接法:
|
||||
- 登录成功下发 `JSESSIONID`,参数为 `Path=/`、`HttpOnly`、`SameSite=Lax`,14 天滑动过期。
|
||||
**`Path=/` 是必须的**:nginx 会把 `/user` 改写成后端 `/personal/user`,若沿用容器按
|
||||
请求路径推导的 `/personal`,浏览器判定 `/user` 不匹配就不会带会话,面板会一直 401。
|
||||
- `PersonalInterceptor` 放行「有效会话 **或** `AuthCode=alone`」,两者都拒绝时返回 **401**。
|
||||
`alone` 是留给下载器前端与存储节点 `/message2me` 推送的,本次**没有**退役它。
|
||||
|
||||
- 接法 A(推荐,改动最小):PersonalHub 复用已有的 `_login_handler` 形状,新增一个
|
||||
「生成 LionWebsite 登录链接」的处理函数。它需要拿到一张 LionWebsite 票据,因此
|
||||
LionWebsite 侧要提供一个**受保护的签发端点**,例如 `POST /personal/login/ticket`,
|
||||
用现有共享盘内部令牌或固定内部密钥鉴权,返回一次性票据;PersonalHub 调用后拼成链接。
|
||||
- 接法 B(无新增网络调用):两端约定一个共享密钥,PersonalHub 本地用
|
||||
`HMAC-SHA256(密钥, 时间戳)` 自签票据,LionWebsite 校验签名与时间窗。省一次调用,
|
||||
但票据在时间窗内可重放,需要额外一次性状态才能封住;而 PersonalHub 现有
|
||||
`OneTimePanelLoginTickets` 本来就是进程内一次性表,接法 A 更贴近既有做法。
|
||||
### 1.5 密钥
|
||||
|
||||
两种接法下,QQ 与 TG 都走同一个处理函数,`TELEGRAM_COMMANDS` 加 `login`,
|
||||
QQ 侧加 `/login`(别名 `/登录`),回复文案沿用 PersonalHub 的
|
||||
「已生成一次性登录链接,X 分钟内有效且只能使用一次……请勿转发」。
|
||||
- 主站从环境变量 `PERSONAL_LOGIN_SECRET` 读取(`personal.login.secret`),
|
||||
由 systemd drop-in `/etc/systemd/system/lionwebsite.service.d/login-secret.conf`
|
||||
加载 `/etc/lionwebsite/login-secret.env`(`600` 权限,不在仓库里)。
|
||||
- 密钥为空时**一律拒绝**票据,不会退化成放行。
|
||||
- 轮换方式:改两侧配置并重启。轮换后旧票据在 300 秒内自然失效。
|
||||
|
||||
跨主机连通性:us9929 与 vm103 之间已有 EasyTier(us9929 侧 `10.0.0.6`,
|
||||
vm103 即 `192.168.0.204`),且 `personal.lionwebsite.xyz` 直接解析到 us9929 公网 IP
|
||||
`38.60.92.138`,不经过 Cloudflare。因此接法 A 的内网调用与浏览器打开链接都不需要
|
||||
额外放通。
|
||||
## 2. 机器人侧待实施(host-vm103-debian-qq)
|
||||
|
||||
### 3.3 前端(`PrivateMain` 桌面 / `PrivateMainForMobile` 移动)
|
||||
### 2.1 生成票据
|
||||
|
||||
- 删除 `authCode: "alone"` 常量与 `?AuthCode=` 查询串,改为依赖同源 Cookie
|
||||
(axios 同源请求默认携带 Cookie)。
|
||||
- 未登录或会话过期时显示提示页「登录已过期,请在机器人里发送 /login」,不要静默失败。
|
||||
- 下载器前端(`lionwebsite-frontend-desktop` / `-mobile`)使用每个用户自己的授权码,
|
||||
本次不动。
|
||||
密钥必须与主站一致,存 PersonalHub 的秘密库(共享盘只留 `secret://` 引用),
|
||||
不要写进仓库或聊天记录。Python 侧计算方式:
|
||||
|
||||
### 3.4 收尾:退役 `alone`
|
||||
```python
|
||||
import hashlib, hmac, time
|
||||
|
||||
- `PersonalInterceptor`、`AdaptorFilter` 不再比较字面量;移动端跳转改为原样透传查询串。
|
||||
- 存储节点两处 `alone` 换成配置项,属 storageNode 仓库,单独一次发布。
|
||||
- 轮换后确认无调用方仍依赖旧字面量。
|
||||
def lionwebsite_login_link(secret: str, base: str = "https://personal.lionwebsite.xyz") -> str:
|
||||
stamp = str(int(time.time()))
|
||||
signed = f"v1.{stamp}"
|
||||
digest = hmac.new(secret.encode(), signed.encode(), hashlib.sha256).hexdigest()
|
||||
return f"{base}/login?t=v1.{stamp}.{digest}"
|
||||
```
|
||||
|
||||
## 4. 备选与取舍
|
||||
Shell 等价写法(`openssl`)供人工验证用:
|
||||
|
||||
- 方案 B(最小改动):前端增加「从 URL 读取 `AuthCode` 并记住」,机器人 `/login` 直接回
|
||||
`https://personal.lionwebsite.xyz/index?AuthCode=alone`。半天可上线,但固定密钥仍会进
|
||||
聊天记录、浏览器历史与 nginx 日志,也没解决 `alone` 写死的问题。
|
||||
- 方案 C(只治理配置):把 `alone` 从代码搬到配置并轮换,安全提升有限。
|
||||
- 口令 + TOTP 登录页:PersonalHub 有这一套,但用户明确要的是「输入 /login 就弹出登录
|
||||
地址」,口令 TOTP 属于另一条路径。如果以后想在电脑上直接登录而不经机器人,可以再补,
|
||||
两者共用同一个会话状态即可。
|
||||
```sh
|
||||
SIG=$(printf 'v1.%s' "$STAMP" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $NF}')
|
||||
```
|
||||
|
||||
## 5. 需要确认的三点
|
||||
### 2.2 命令接入
|
||||
|
||||
1. 采用推荐方案(一次性票据 + 会话),还是先上备选 B?
|
||||
2. 登录链接只允许主人使用,还是允许机器人给其他授权用户分别签发(对应 `User` 表账号)?
|
||||
3. 会话有效期:PersonalHub 是 14 天,建议 LionWebsite 也用 14 天滑动过期,可调整。
|
||||
- Telegram:`TELEGRAM_COMMANDS` 增加 `login`,并复用 PersonalHub 现成的
|
||||
`configure_handlers(login_handler=...)` 形状;文案可沿用
|
||||
「已生成登录链接,5 分钟内有效,请勿转发」。
|
||||
- QQ(LionQQBot):增加 `/login`(别名 `/登录`),走同一套生成逻辑。
|
||||
- 按项目既有约定,新命令必须同时覆盖 QQ 与 Telegram,帮助菜单同步更新。
|
||||
|
||||
## 6. 实施顺序
|
||||
### 2.3 与 PersonalHub 现有 /login 的关系
|
||||
|
||||
1. 后端票据签发与会话校验、拦截器放行、退出端点,含单元测试。
|
||||
2. 后端受保护的票据签发端点(接法 A)或共享密钥签名(接法 B)。
|
||||
3. 前端去掉 `alone`,补未登录提示。
|
||||
4. 机器人 QQ 与 Telegram 双向 `/login`。
|
||||
5. 存储节点两处 `alone` 改为配置。
|
||||
6. 退役字面量 `alone`。
|
||||
PersonalHub 已有一个 `/login`,但它签发的是**它自己面板**的票据
|
||||
(`OneTimePanelLoginTickets` + `/panel/telegram-login`),与 LionWebsite 无关。
|
||||
接入时二选一,建议前者:
|
||||
|
||||
1. `/login` 仍只回 PersonalHub 面板链接,另加 `lionlogin`(或 `/面板`)专给
|
||||
LionWebsite —— 语义清晰,不改动既有命令行为;
|
||||
2. 让 `/login` 一条消息里同时给出两个链接 —— 少一个命令,但会改动现有文案与测试。
|
||||
|
||||
## 3. 可选收尾(未做,需要时另开任务)
|
||||
|
||||
- 移动端 `PrivateMainForMobile` 仍写死 `authCode: "alone"`。后端两种方式都接受,
|
||||
所以它能用;要一并切到会话,照搬 `PrivateMain/src/store/index.js` 的改法即可。
|
||||
- `sourcecode/storageNode` 有两处硬编码 `alone`(`CustomUtil.java` 的 `/message2me`
|
||||
与 `MultiThreadedHTTPServer.java` 的本机鉴权),属另一仓库,需单独发布后才能退役字面量。
|
||||
- 概览页的「本机订阅」链接 `https://personal.lionwebsite.xyz/sub/self` 目前是 404,
|
||||
后端没有对应映射,属历史遗留,与本次登录改造无关。
|
||||
|
||||
Reference in New Issue
Block a user