diff --git a/src/auth.ts b/src/auth.ts index cbfe5e8..993728e 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -22,6 +22,9 @@ export const auth = { apiKey(): string { return storage.getItem(KEY) ?? '' }, setKey(key: string) { storage.setItem(KEY, key.trim()) }, clear() { storage.removeItem(KEY) }, + // 密码登录成功后切换到 HttpOnly 会话 cookie:必须清掉残留 Bearer——服务端 + // 认证中间件 Bearer 优先于会话 cookie,失效残留 key 会把有效会话也判成 401。 + beginSession() { this.clear() }, // 带凭证的 fetch:未设置密钥时与原生 fetch 等价(嵌入式档无认证)。 fetch(input: string, init?: RequestInit): Promise { const key = this.apiKey() diff --git a/src/components/LoginPage.tsx b/src/components/LoginPage.tsx index 757475c..0ba4a5c 100644 --- a/src/components/LoginPage.tsx +++ b/src/components/LoginPage.tsx @@ -41,6 +41,8 @@ export function LoginPage({ onAuthed, onMustChange }: { onAuthed: () => void; on setErr(''); setBusy(true) try { const me = await api.login(owner.trim(), password) + // 会话凭证是 HttpOnly cookie;清残留 Bearer,避免其遮蔽有效会话。 + auth.beginSession() if (me.mustChangePassword) { // 初始/重置密码:先完成强制改密再放行。 onMustChange(me.owner) diff --git a/test/auth.test.ts b/test/auth.test.ts index 112c9ea..4491492 100644 --- a/test/auth.test.ts +++ b/test/auth.test.ts @@ -40,4 +40,15 @@ describe('auth 凭证模块', () => { expect(auth.apiKey()).toBe('') expect(auth.withKey('/x')).toBe('/x') }) + + it('beginSession:密码会话生效后清残留 Bearer,fetch 不再带头', async () => { + auth.setKey('ask_stale') + auth.beginSession() + expect(auth.apiKey()).toBe('') + const spy = vi.fn().mockResolvedValue(new Response('{}')) + vi.stubGlobal('fetch', spy) + await auth.fetch('/api/me') + const [, init] = spy.mock.calls[0] as [string, RequestInit | undefined] + expect(new Headers(init?.headers ?? {}).get('Authorization')).toBeNull() + }) })