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