138 lines
6.9 KiB
Markdown
138 lines
6.9 KiB
Markdown
# LionWebsite 一键登录(/login):实现说明
|
||
|
||
状态:**已全部完成并上线**。主站侧提交 `00e2715`(文档 `dfb647b`、`628f3ad`),
|
||
桌面端 `9b5bc94`;机器人侧由 `debian-qq-chatgpt` 于 host-vm103-debian-qq 完成
|
||
(见共享盘 `tasks/2026-09-21-lionwebsite-login-bot-command/`)。
|
||
关联待办:`td-20260921-a1b2c3`(已归档为
|
||
`todos/archive/done-td-20260921-a1b2c3-lionwebsite-login-tg.md`)。
|
||
|
||
用户已明确选择**接法 B:共享密钥自签票据**,理由是机器人到主站的网络调用不可靠。
|
||
因此主站**不提供**任何票据签发端点,也不需要机器人侧发起 HTTP 请求——机器人本地
|
||
用共享密钥算出票据即可。
|
||
|
||
## 1. 已经上线的东西(主站侧)
|
||
|
||
### 1.1 票据格式
|
||
|
||
```
|
||
v1.<签发时间的 Unix 秒级时间戳>.<HMAC-SHA256 十六进制小写>
|
||
```
|
||
|
||
签名原文为 `v1.<时间戳>`(固定前缀 `v1.` 加时间戳本身),密钥为共享密钥的 UTF-8 字节。
|
||
等价于 `hex(HMAC_SHA256(key, "v1." + timestamp))`。
|
||
|
||
### 1.2 校验规则
|
||
|
||
- 必须恰好三段,第一段必须是 `v1`;
|
||
- 签名比较用常量时间(`MessageDigest.isEqual`);
|
||
- 时间戳只接受 `[now - 300s, now]`,**未来时间戳一律拒绝**(防伪造者用远期时间换长期有效);
|
||
- 窗口内**允许重放**(用户明确接受),过期即失效;
|
||
- 默认 300 秒有效期,可用 `personal.login.ticket-ttl-seconds` 调整,下限 30 秒。
|
||
|
||
### 1.3 端点
|
||
|
||
| 端点 | 作用 |
|
||
| --- | --- |
|
||
| `GET /login?t=<票据>` | 校验票据 → 作废旧会话(防会话固定)→ 建新会话 → 302 跳 `/index`;失败 302 跳 `/denied` |
|
||
| `GET /login/logout` | 销毁会话 → 302 跳 `/denied` |
|
||
| `GET /denied` | 静态提示页:「请在机器人里发送 `/login`」 |
|
||
|
||
nginx 无需改动:`location /` 会把 `/login` 改写成后端 `/personal/login`。
|
||
|
||
### 1.4 会话与鉴权
|
||
|
||
- 登录成功下发 `JSESSIONID`,参数为 `Path=/`、`HttpOnly`、`SameSite=Lax`,14 天滑动过期。
|
||
**`Path=/` 是必须的**:nginx 会把 `/user` 改写成后端 `/personal/user`,若沿用容器按
|
||
请求路径推导的 `/personal`,浏览器判定 `/user` 不匹配就不会带会话,面板会一直 401。
|
||
- `PersonalInterceptor` 放行「有效会话 **或** `AuthCode=alone`」,两者都拒绝时返回 **401**。
|
||
`alone` 是留给下载器前端与存储节点 `/message2me` 推送的,本次**没有**退役它。
|
||
|
||
### 1.5 密钥
|
||
|
||
- 主站从环境变量 `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 秒内自然失效。
|
||
|
||
## 2. 机器人侧待实施(host-vm103-debian-qq)
|
||
|
||
### 2.1 生成票据
|
||
|
||
密钥必须与主站一致。**已由主人批准写入 PersonalHub 保险库**,引用为
|
||
`secret://services/service-lionwebsite/panel-login-secret`;机器人侧按既有秘密读取流程
|
||
申请取值,不要写进仓库或聊天记录。Python 侧计算方式:
|
||
|
||
```python
|
||
import hashlib, hmac, time
|
||
|
||
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}"
|
||
```
|
||
|
||
Shell 等价写法(`openssl`)供人工验证用:
|
||
|
||
```sh
|
||
SIG=$(printf 'v1.%s' "$STAMP" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $NF}')
|
||
```
|
||
|
||
### 2.2 命令接入(已实施:复用既有 `/login`)
|
||
|
||
**复用既有 `/login`,不新建命令名;一条回复里同时给出两个登录链接。**
|
||
|
||
实现方式(vm103 实际做法,比原计划更稳):**机器人进程不持有密钥**。
|
||
签发走保险库固定动作 `CollabSecretService.use_secret`,值只交给进程内签名函数,
|
||
不返回、不落盘、不进日志;审计只记引用与动作名 `lionwebsite-login-link`。
|
||
QQ 侧 `plugins/login.py` 只经内部令牌 POST `/internal/panel-login`,
|
||
由 PersonalService 统一产出文案,因此 QQ 与 Telegram 不会漂移。
|
||
|
||
- Telegram:`telegram_panel_login()` 的返回文案改为同时给出 PersonalHub 面板链接与
|
||
LionWebsite 面板链接,`TELEGRAM_COMMANDS` 里 `login` 的描述同步更新。
|
||
- QQ(LionQQBot):既有 `/login`(别名 `/登录`)返回同样两条链接,共用同一套生成逻辑。
|
||
- PersonalHub 未完整配置(缺 password / session_secret / TOTP)时,现有逻辑会回一句
|
||
配置提示;此时**仍要发出 LionWebsite 链接**,不要让一条链接的失败带掉另一条。
|
||
- 帮助菜单(Telegram `/help` 与 QQ 菜单)中 `/login` 的描述同步更新。
|
||
- 文案不要写成「两条都只能用一次」:PersonalHub 链接是一次性的,
|
||
LionWebsite 票据是 300 秒窗口内可重放。
|
||
|
||
示例文案:
|
||
|
||
```
|
||
已生成登录链接:
|
||
|
||
· Personal Hub:[打开面板](< PersonalHub 链接 >)
|
||
· LionWebsite:[打开面板](< LionWebsite 链接 >)
|
||
|
||
两条链接 5 分钟内有效,请勿转发。
|
||
```
|
||
|
||
### 2.3 与 PersonalHub 现有 /login 的关系
|
||
|
||
PersonalHub 的 `/login` 原本只签发**它自己面板**的票据
|
||
(`OneTimePanelLoginTickets` + `/panel/telegram-login`)。改造时保留这条,
|
||
再在同一回复里附加 LionWebsite 链接;两者的票据机制与有效期互不影响。
|
||
|
||
## 3. 交付与复验
|
||
|
||
- 机器人侧提交状态:**两个机器人仓库的改动尚未提交 Git**(`v103` 侧待主人确认后
|
||
按项目约定提交,注意 PersonalHub 工作区另有前一轮未提交改动,勿混提)。
|
||
- `us9929main-chatgpt` 的独立复验(2026-09-21):
|
||
- 票据边界实测:`now` 与 `now-290s` 放行,`now-301s`、`now-400s`、`now+60s` 均落 `/denied`;
|
||
篡改签名与畸形串同样落 `/denied`。
|
||
- 密钥明文扫描:抓取两个任务目录全部正文与线程消息共 17 KiB,未出现密钥明文,
|
||
只有 4 处 `secret://` 引用。
|
||
- 内部接口鉴权:`POST /internal/panel-login` 无令牌与错令牌均返回 403。
|
||
- PersonalHub `/health` 返回 200;共享盘 waiting 指针已消费,队列已清空。
|
||
|
||
## 4. 可选收尾(未做,需要时另开任务)
|
||
|
||
- 移动端 `PrivateMainForMobile` 仍写死 `authCode: "alone"`。后端两种方式都接受,
|
||
所以它能用;要一并切到会话,照搬 `PrivateMain/src/store/index.js` 的改法即可。
|
||
- `sourcecode/storageNode` 有两处硬编码 `alone`(`CustomUtil.java` 的 `/message2me`
|
||
与 `MultiThreadedHTTPServer.java` 的本机鉴权),属另一仓库,需单独发布后才能退役字面量。
|
||
- 概览页的「本机订阅」链接 `https://personal.lionwebsite.xyz/sub/self` 目前是 404,
|
||
后端没有对应映射,属历史遗留,与本次登录改造无关。
|