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

6.9 KiB
Raw Blame History

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 侧计算方式:

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)供人工验证用:

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, 后端没有对应映射,属历史遗留,与本次登录改造无关。