浏览器免重登:HttpOnly cookie + 服务端 session 表 + 设备名持久化

- 新增 web_sessions 表(sqlc):refresh_token 以 AES-256-GCM 落盘,密钥取自
  CDROP_SESSION_SECRET(env、不在库内);cookie 仅存不透明随机串,表 id 存其
  SHA-256,故 DB 单独泄露既换不出可用 cookie、也解不出 token
- /auth/exchange 建 session 并下发 HttpOnly; Secure; SameSite=Lax; Path=/api/auth
  cookie,响应体不再回传 refresh_token
- /auth/refresh 改为 cookie 驱动(7 天滑动失活),同时即开机静默免重登;
  新增 /auth/logout(删 session + 清 cookie)、/auth/device(写设备名入 session)
- device_name 随 session 存,开机 refresh 带回前端,抗 iOS PWA 存储清除;
  setup / 改名时 syncWebDeviceName 推送服务端
- striped per-session 锁串行化同会话并发 refresh,防一次性 refresh_token 被
  花两次而把用户从所有 tab 登出
- 前端 store 移除 refreshToken 字段(长寿命凭据彻底不进 JS);main.tsx 首屏前
  bootstrapAuth 用 cookie 静默续期,命中直接进已登录态、避免登录页闪现
- config:prod 强制 CDROP_SESSION_SECRET,缺失拒启动
- 桌面端不受影响(自有 loopback flow + Go keyring,不碰这两个端点)
This commit is contained in:
2026-06-16 00:54:40 +08:00
parent 8f550adbb1
commit e51666c52e
20 changed files with 874 additions and 81 deletions
+22 -9
View File
@@ -13,6 +13,7 @@ import { RouterProvider, createRouter } from "@tanstack/react-router";
import { ToastViewport } from "./ui/feedback";
import { routeTree } from "./routeTree.gen";
import { useAppStore, type ThemeMode } from "./store";
import { bootstrapAuth } from "./features/auth/auth";
import { startCjkAutospace } from "./utils/cjkAutospace";
import { initDesktopMenuBridge, isDesktop, loadDesktopSettings } from "./net/desktop";
@@ -115,16 +116,28 @@ function App()
// 时从注入的 window.__CDROP_BOOT__ 同步水合(store/helpers.ts),无需异步等待。
applyTheme(useAppStore.getState().theme);
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
);
function mount(): void
{
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
);
// CJK 自动间距 shim:在 React 完成首次挂载后启动。MutationObserver 后续
// 增量处理 React 重渲染产生的新文本节点。放在 createRoot 之后,首帧绘制
// 完会触发一次 runShim() 处理已挂载的子树。
requestAnimationFrame(() => { startCjkAutospace(); });
}
// CJK 自动间距 shim:在 React 完成首次挂载后启动。MutationObserver 后续
// 增量处理 React 重渲染产生的新文本节点。放在 createRoot 之后,首帧绘制
// 完会触发一次 runShim() 处理已挂载的子树。
requestAnimationFrame(() => { startCjkAutospace(); });
// 浏览器端「免重登」:首屏渲染前用 HttpOnly cookie 尝试静默续期。命中则直接以已
// 登录态挂载,避免登录页 / 设置页闪现;未命中(401)即照常落到登录页。桌面 / dev /
// sessionStorage 已有令牌时 bootstrapAuth 立即返回,几乎无延迟。4s 超时兜底:万一
// refresh 卡死,也不至于把整个应用永久挡在首屏外。
const BOOT_AUTH_TIMEOUT_MS = 4000;
void Promise.race([
bootstrapAuth().catch(() => { /* ignore:失败即落登录页 */ }),
new Promise<void>((resolve) => { window.setTimeout(resolve, BOOT_AUTH_TIMEOUT_MS); }),
]).finally(mount);
// PWA service worker:仅在浏览器 + prod 注册。dev 下会干扰 Vite HMR;桌面壳
// Wails WebViewwails:// / loopback origin)已是原生应用且 SW 行为不可靠,