请求路径
调用栈走读
挂载时,这个 React provider 发出一个 GET /api/session 请求,并带上 credentials: 'include',让 fw_sid cookie 一起附上。响应要么把 currentUser 注入到 context,要么留为 null——后者被路由识别为"展示登录界面"。
查看源码
// src/app/providers/AuthProvider.tsx export function AuthProvider({ children }: Props) { const [user, setUser] = useState<User | null>(null); useEffect(() => { fetch('/api/session', { credentials: 'include' }) .then(r => r.ok ? r.json() : null) .then(setUser); }, []); return <AuthCtx.Provider value={{ user }}>{children}</AuthCtx.Provider>; }
这个路由本身很薄:它只是把中间件挂到 req.ctx.session 上的内容原样返回。如果中间件已经短路返回 401,这个处理器根本不会执行——所以这里没有重复的认证逻辑。
查看源码
// src/server/routes/session.ts router.get('/session', verifyToken, (req, res) => { const { session } = req.ctx; res.json({ id: session.userId, email: session.email, role: session.role, exp: session.expiresAt, }); });
这里就是信任边界。verifyToken 读取已签名的 fw_sid cookie,请 SessionStore 解析它,要么把结果挂到 req.ctx.session 上,要么返回 401。应用里所有受保护的路由都挂在这个函数后面,所以改它的行为,就等于全局改变了认证。
查看源码
// src/middleware/auth.ts export async function verifyToken(req, res, next) { const raw = req.signedCookies['fw_sid']; if (!raw) return res.status(401).end(); const session = await SessionStore.get(raw); if (!session || session.expiresAt < Date.now()) { return res.status(401).end(); } req.ctx = { session }; next(); }
SessionStore 是一个小巧的"读穿透"缓存:先查进程内的 LRU,未命中再回落到 Postgres。写操作(create、revoke)一律直接落库,并使对应缓存项失效,避免其他 worker 端出过期会话。
查看源码
// src/lib/sessionStore.ts const cache = new LRU<string, Session>({ max: 5000, ttl: 60_000 }); export const SessionStore = { async get(id: string) { const hit = cache.get(id); if (hit) return hit; const row = await db.one(SELECT_SESSION, [id]); if (row) cache.set(id, row); return row ?? null; }, /* create、revoke、touch ... */ };
sessions 表的主键是一个随机 32 字节的 id(即 cookie 值),并在 user_id 上建有覆盖索引,用于"全部设备登出"。过期时间在这里(expires_at)和中间件里都做了校验,作为纵深防御。
查看源码
-- db/migrations/004_sessions.sql create table sessions ( id text primary key, user_id uuid not null references users(id), created_at timestamptz default now(), expires_at timestamptz not null, ip inet, user_agent text ); create index sessions_user_id_idx on sessions(user_id);