From 9f225684b58df95d067112fd286c5d636214995b Mon Sep 17 00:00:00 2001 From: kdletters Date: Mon, 20 Apr 2026 09:26:20 +0000 Subject: [PATCH] =?UTF-8?q?=E7=A7=BB=E9=99=A4=E5=89=8D=E7=AB=AF=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E6=B8=B8=E5=AE=A2=E5=87=AD=E8=AF=81=E6=AE=8B=E7=95=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/technical/README.md | 1 + ...UTO_GUEST_CREDENTIAL_REMOVAL_2026-04-20.md | 68 +++++++++++++++++++ src/components/auth/AuthGate.test.tsx | 6 +- src/components/auth/AuthGate.tsx | 2 +- src/services/apiClient.ts | 42 ------------ src/services/authService.test.ts | 10 --- src/services/authService.ts | 51 +------------- 7 files changed, 73 insertions(+), 107 deletions(-) create mode 100644 docs/technical/STDB_AUTH_TAIL_PHASE1_AUTO_GUEST_CREDENTIAL_REMOVAL_2026-04-20.md diff --git a/docs/technical/README.md b/docs/technical/README.md index b768c952..a3c56e3d 100644 --- a/docs/technical/README.md +++ b/docs/technical/README.md @@ -9,6 +9,7 @@ - [PROMPT_DIRECTORY_MANAGEMENT_2026-04-19.md](./PROMPT_DIRECTORY_MANAGEMENT_2026-04-19.md):后端提示词收口到 `server-node/src/prompts/` 的目录方案、兼容策略与后续新增规则。 - [SPACETIME_DEV_URI_HOTFIX_2026-04-20.md](./SPACETIME_DEV_URI_HOTFIX_2026-04-20.md):修复开发默认配置把 Spacetime 连接误指向 Vite `3000` 端口的问题。 - [SPACETIME_AUTH_TOKEN_FALLBACK_HOTFIX_2026-04-20.md](./SPACETIME_AUTH_TOKEN_FALLBACK_HOTFIX_2026-04-20.md):本地 token 失效时自动降级匿名连接,并提示“登录已过期”的热修记录。 +- [STDB_AUTH_TAIL_PHASE1_AUTO_GUEST_CREDENTIAL_REMOVAL_2026-04-20.md](./STDB_AUTH_TAIL_PHASE1_AUTO_GUEST_CREDENTIAL_REMOVAL_2026-04-20.md):Auth 尾巴清理第一段,删除前端自动游客用户名/密码残留。 - [TASK_AUTO_COMMIT_WORKFLOW_2026-04-20.md](./TASK_AUTO_COMMIT_WORKFLOW_2026-04-20.md):任务完成后按文件边界自动提交的脚本与协作约定。 - [NODE_DEV_STARTUP_HOTFIX_2026-04-20.md](./NODE_DEV_STARTUP_HOTFIX_2026-04-20.md):`npm run dev` 启动失败的热修记录、根因与验证结果。 - [NODE_SERVER_KNOWLEDGE_GRAPH_2026-04-08.md](./NODE_SERVER_KNOWLEDGE_GRAPH_2026-04-08.md):当前 Node 运行时后端的技术栈、入口、鉴权、存储与接口知识图谱。 diff --git a/docs/technical/STDB_AUTH_TAIL_PHASE1_AUTO_GUEST_CREDENTIAL_REMOVAL_2026-04-20.md b/docs/technical/STDB_AUTH_TAIL_PHASE1_AUTO_GUEST_CREDENTIAL_REMOVAL_2026-04-20.md new file mode 100644 index 00000000..7e1fb7c6 --- /dev/null +++ b/docs/technical/STDB_AUTH_TAIL_PHASE1_AUTO_GUEST_CREDENTIAL_REMOVAL_2026-04-20.md @@ -0,0 +1,68 @@ +# STDB Auth 尾巴清理 Phase 1:移除前端自动游客凭证残留(2026-04-20) + +## 1. 本轮目标 + +这轮只处理认证迁移里最安全、且不会打断现有 `/api/runtime/story/*` 的一段尾巴: + +1. 删除前端本地自动游客用户名/密码残留 +2. 保留当前 `ACCESS_TOKEN_KEY`,因为它仍同时承载: + - Spacetime token + - 旧 `/api/runtime/story/*` Bearer token +3. 不处理 Node JWT middleware 本身 +4. 不处理 runtime story 向 STDB 的正式迁移 + +## 2. 背景问题 + +当前前端虽然已经用 `ensureAutoAuthUser()` 走 STDB 匿名连接,但代码里仍保留一套旧时代残留: + +1. `AutoAuthCredentials` +2. `createAutoAuthCredentials()` +3. `authEntryWithStoredCredentials()` +4. `apiClient.ts` 中的 `AUTO_AUTH_USERNAME_KEY / AUTO_AUTH_PASSWORD_KEY` + +这些内容已经不再承担真实登录能力,只会继续制造两个误导: + +1. 让人误以为当前游客登录仍依赖浏览器本地用户名/密码恢复 +2. 让 STDB token 收口与旧 JWT 清理边界持续混在一起 + +## 3. 本轮落地 + +代码调整: + +1. `src/services/authService.ts` + - 删除 `AutoAuthCredentials` + - 删除 `createAutoAuthCredentials()` + - 删除 `authEntryWithStoredCredentials()` + - `ensureAutoAuthUser()` 改为只返回 `AuthUser` + - `clearAuthSession()` / `logoutAuthUser()` / `logoutAllAuthSessions()` 不再清理自动游客凭证 +2. `src/services/apiClient.ts` + - 删除自动游客用户名/密码的 localStorage key 和相关 helper +3. `src/components/auth/AuthGate.tsx` + - 匿名建连成功后直接消费 `AuthUser` +4. 测试同步调整: + - `src/components/auth/AuthGate.test.tsx` + - `src/services/authService.test.ts` + +## 4. 当前边界 + +这轮刻意不动: + +1. `ACCESS_TOKEN_KEY` +2. `/api/auth/refresh` +3. `fetchWithApiAuth()` 的 Bearer 注入 +4. `server-node/src/middleware/auth.ts` + +原因很简单: + +1. 当前 `runtimeStoryService.ts` 仍在访问 `/api/runtime/story/*` +2. Express 这条链路仍要求旧 JWT Bearer +3. 如果现在直接删 `ACCESS_TOKEN_KEY` 语义,会把 STDB token 与 runtime story 旧链路一起打断 + +因此这轮是 Auth 尾巴清理的第一段,不是最终收口。 + +## 5. 下一步建议 + +下一步应继续处理: + +1. 为 STDB token 与旧 HTTP bearer 拆出独立存储语义 +2. 或直接推进 `runtimeStoryService.ts` 迁到 STDB,随后删除 `/api/runtime/story/*` 对旧 JWT 的依赖 diff --git a/src/components/auth/AuthGate.test.tsx b/src/components/auth/AuthGate.test.tsx index 17ec8c34..721fb488 100644 --- a/src/components/auth/AuthGate.test.tsx +++ b/src/components/auth/AuthGate.test.tsx @@ -99,11 +99,7 @@ beforeEach(() => { }); authMocks.startWechatLogin.mockResolvedValue(undefined); authMocks.ensureAutoAuthUser.mockResolvedValue({ - user: mockUser, - credentials: { - username: 'guest_tester', - password: 'auto_password', - }, + ...mockUser, }); }); diff --git a/src/components/auth/AuthGate.tsx b/src/components/auth/AuthGate.tsx index 3b34020c..751b1383 100644 --- a/src/components/auth/AuthGate.tsx +++ b/src/components/auth/AuthGate.tsx @@ -170,7 +170,7 @@ export function AuthGate({ children }: AuthGateProps) { setStatus('recovering'); try { - const { user: nextUser } = await ensureAutoAuthUser(); + const nextUser = await ensureAutoAuthUser(); if (!isActive) { return; } diff --git a/src/services/apiClient.ts b/src/services/apiClient.ts index e508c46a..f739c682 100644 --- a/src/services/apiClient.ts +++ b/src/services/apiClient.ts @@ -9,8 +9,6 @@ import { } from '../../packages/shared/src/http'; const ACCESS_TOKEN_KEY = 'genarrative.auth.access-token.v1'; -const AUTO_AUTH_USERNAME_KEY = 'genarrative.auth.auto-username.v1'; -const AUTO_AUTH_PASSWORD_KEY = 'genarrative.auth.auto-password.v1'; export const AUTH_STATE_EVENT = 'genarrative-auth-state-changed'; const REQUEST_ID_HEADER = 'x-request-id'; const API_VERSION_HEADER = 'x-api-version'; @@ -376,46 +374,6 @@ export function clearStoredAccessToken( } } -export function getStoredAutoAuthCredentials() { - if (!canUseLocalStorage()) { - return null; - } - - const username = window.localStorage.getItem(AUTO_AUTH_USERNAME_KEY)?.trim() || ''; - const password = window.localStorage.getItem(AUTO_AUTH_PASSWORD_KEY)?.trim() || ''; - - if (!username || !password) { - return null; - } - - return { - username, - password, - }; -} - -export function setStoredAutoAuthCredentials(credentials: { - username: string; - password: string; -}) { - if (!canUseLocalStorage()) { - return; - } - - window.localStorage.setItem(AUTO_AUTH_USERNAME_KEY, credentials.username.trim()); - window.localStorage.setItem(AUTO_AUTH_PASSWORD_KEY, credentials.password.trim()); -} - -export function clearStoredAutoAuthCredentials() { - if (!canUseLocalStorage()) { - return; - } - - window.localStorage.removeItem(AUTO_AUTH_USERNAME_KEY); - window.localStorage.removeItem(AUTO_AUTH_PASSWORD_KEY); - emitAuthStateChange(); -} - function withAuthorizationHeaders( headers?: HeadersInit, options: Pick = {}, diff --git a/src/services/authService.test.ts b/src/services/authService.test.ts index 736e5875..3036ef57 100644 --- a/src/services/authService.test.ts +++ b/src/services/authService.test.ts @@ -3,11 +3,9 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { ApiClientError, clearStoredAccessToken, - clearStoredAutoAuthCredentials, setStoredAccessToken, } from './apiClient'; import { - createAutoAuthCredentials, getAuthRiskBlocks, getAuthSessions, getCaptchaChallengeFromError, @@ -185,14 +183,6 @@ describe('authService with SpacetimeDB', () => { spacetimeMocks.ensureSpacetimeConnection.mockReset(); spacetimeMocks.disconnectSpacetimeConnection.mockReset(); clearStoredAccessToken(); - clearStoredAutoAuthCredentials(); - }); - - it('creates credentials that match current guest username/password constraints', () => { - const credentials = createAutoAuthCredentials(); - - expect(credentials.username).toMatch(/^guest_[a-z0-9]{12}$/u); - expect(credentials.password).toMatch(/^auto_[a-z0-9]{24}_[a-z0-9]{8}$/u); }); it('extracts captcha challenge details from api errors', () => { diff --git a/src/services/authService.ts b/src/services/authService.ts index ae32e850..cf35f5ed 100644 --- a/src/services/authService.ts +++ b/src/services/authService.ts @@ -34,18 +34,12 @@ import { import { ApiClientError, clearStoredAccessToken, - clearStoredAutoAuthCredentials, getStoredAccessToken, } from './apiClient'; export type { AuthUser } from '../../packages/shared/src/contracts/auth'; export type { AuthLoginMethod } from '../../packages/shared/src/contracts/auth'; -export type AutoAuthCredentials = { - username: string; - password: string; -}; - export type AuthSessionSnapshot = { user: import('../../packages/shared/src/contracts/auth').AuthUser | null; availableLoginMethods: AuthLoginMethod[]; @@ -65,30 +59,10 @@ export type ConsumedAuthCallback = { error: string | null; }; -let pendingAutoAuthUser: Promise<{ - user: AuthUser; - credentials: AutoAuthCredentials; -}> | null = null; +let pendingAutoAuthUser: Promise | null = null; const TOKEN_RECOVERY_TIMEOUT_MS = 3500; -function buildRandomSegment(length: number) { - const alphabet = 'abcdefghijklmnopqrstuvwxyz0123456789'; - const cryptoApi = globalThis.crypto; - - if (!cryptoApi?.getRandomValues) { - return Array.from( - { length }, - () => alphabet[Math.floor(Math.random() * alphabet.length)], - ).join(''); - } - - const bytes = cryptoApi.getRandomValues(new Uint8Array(length)); - return Array.from(bytes, (value) => alphabet[value % alphabet.length]).join( - '', - ); -} - function sleep(ms: number) { return new Promise((resolve) => { window.setTimeout(resolve, ms); @@ -255,17 +229,9 @@ export function getCaptchaChallengeFromError( return null; } -export function createAutoAuthCredentials(): AutoAuthCredentials { - return { - username: `guest_${buildRandomSegment(12)}`, - password: `auto_${buildRandomSegment(24)}_${buildRandomSegment(8)}`, - }; -} - export function clearAuthSession() { disconnectSpacetimeConnection(); clearStoredAccessToken(); - clearStoredAutoAuthCredentials(); } export async function sendPhoneLoginCode( @@ -346,20 +312,9 @@ export async function authEntry(_username: string, _password: string) { return session.user; } -export async function authEntryWithStoredCredentials( - credentials: AutoAuthCredentials, -) { - const user = await authEntry(credentials.username, credentials.password); - return user; -} - export async function ensureAutoAuthUser() { pendingAutoAuthUser ??= (async () => { - const user = await authEntry('guest', 'guest'); - return { - user, - credentials: createAutoAuthCredentials(), - }; + return authEntry('guest', 'guest'); })(); try { @@ -460,7 +415,6 @@ export async function liftAuthRiskBlock(_scopeType: 'phone' | 'ip') { export async function logoutAuthUser() { disconnectSpacetimeConnection({ clearToken: true }); - clearStoredAutoAuthCredentials(); return { ok: true, } satisfies LogoutResponse; @@ -475,7 +429,6 @@ export async function logoutAllAuthSessions() { throw new Error(result.message || '退出全部设备失败'); } disconnectSpacetimeConnection({ clearToken: true }); - clearStoredAutoAuthCredentials(); return { ok: true, } satisfies AuthLogoutAllResponse;