Files
NextEdu/docs/architecture/audit/archive/auth-audit-report.md

66 KiB
Raw Blame History

认证模块审计报告

审计日期2026-06-25 审计范围:src/auth.tssrc/proxy.tssrc/next-auth.d.tssrc/modules/auth/**src/modules/onboarding/**src/app/(auth)/**src/app/(onboarding)/**src/app/api/auth/**src/app/api/onboarding/**src/shared/components/{auth-session-provider,onboarding-gate}.tsxsrc/shared/lib/{auth-guard,session,permissions,role-utils,bcrypt-utils,http-utils,password-policy,password-security-service,rate-limit,login-logger,audit-logger,change-logger}.tssrc/shared/hooks/use-permission.tssrc/shared/types/permissions.ts 审计依据:e:\Desktop\CICD\.trae\rules\project_rules.mddocs/architecture/004_architecture_impact_map.md §2.24/§3/§4/§5、docs/architecture/005_architecture_data.json auth/usersAndAuth 前序报告:docs/architecture/audit/permissions-audit-report.mddocs/architecture/audit/00_summary.md


一、现有实现概要

1.1 文件分布

认证模块共 42 个文件,按职责分布如下:

层级 文件数 主要文件
根目录(应用层) 3 auth.ts(234)、proxy.ts(158)、next-auth.d.ts(24)
认证 UI 模块 modules/auth 3 auth-layout.tsx(32)、login-form.tsx(196)、register-form.tsx(259)
Onboarding 模块 modules/onboarding 5 actions.ts(294)、data-access.ts(146)、schema.ts(84)、types.ts(57)、onboarding-stepper.tsx(451)
(auth) 路由组 7 layout.tsx(5)、login/page.tsx(16)、register/page.tsx(167)、privacy/page.tsx(128)、terms/page.tsx(130+)、error.tsx(23)、not-found.tsx(22)
(onboarding) 路由组 1 onboarding/page.tsx(49)
API 路由 3 api/auth/[...nextauth]/route.ts(2)、api/onboarding/status/route.ts(19)、api/onboarding/complete/route.ts(20)
shared/lib认证辅助 12 auth-guard.ts(78)、session.ts(35)、permissions.ts(304)、role-utils.ts(50)、bcrypt-utils.ts(19)、http-utils.ts(44)、password-policy.ts(119)、password-security-service.ts(94)、rate-limit.ts(119)、login-logger.ts(42)、audit-logger.ts(46)、change-logger.ts(43)
shared/components 2 auth-session-provider.tsx(12)、onboarding-gate.tsx(313已废弃未删除)
shared/hooks 1 use-permission.ts(53)
shared/types 1 permissions.ts(236)
i18n 2 auth.json(31)、onboarding.json(78) — 存在但完全未被 login-form / register-form 使用
e2e 测试 1 tests/e2e/smoke-auth.spec.ts(20) — 与实际 UI 不一致

1.2 数据流

[匿名访问]
  proxy.ts → 检查 token → 未登录 → /login
                          ↓
              LoginForm → signIn("credentials")
                          ↓
              auth.ts authorize → rate-limit → user 查询 → 锁定校验 → bcrypt 比对 → 2FA 校验 → 角色/权限加载
                          ↓
              jwt callback → 写入 token.{id,roles,permissions,onboarded}
                          ↓
              proxy.ts → onboarding 检查 → 未完成 → /onboarding
                                              ↓
                                  OnboardingStepper → completeOnboardingAction → data-access + classes data-access
                                              ↓
                                  users.onboardedAt = now → jwt refresh → 默认路径

[已认证访问]
  proxy.ts → 检查 ROUTE_PERMISSIONS → 通过 → 页面 requirePermission → getAuthContext → resolveDataScoperbac resolver→ data-access

1.3 架构图完整性

docs/architecture/004_architecture_impact_map.md §2.24auth 认证 UI 模块)与 §3根目录 auth.ts/proxy.ts记录的导出函数、依赖关系与实际代码大体一致,但存在以下遗漏:

  1. 未记录 modules/onboarding:架构图 §2.24 仅描述 modules/auth,但 onboarding 与 auth 强耦合(首次登录引导),未在 auth 章节中关联说明,仅在 §5 usersAndAuth 域中提及。
  2. 未记录 auth.tsmodules/settings 的反向依赖auth.ts#L106 await import("@/modules/settings/actions-security") 调用 verifyTwoFactorForLogin,但架构图 §3 描述 auth.ts 仅依赖 shared/lib/*,遗漏了跨层依赖。
  3. 未记录 proxy.tsresolveDefaultPath 函数:导出函数仅记录 proxy 主入口,但 resolveDefaultPathonboarding/data-access.ts 重复实现(resolveDefaultPathByRoles),属于逻辑重复未在依赖矩阵中标记。
  4. 未记录 onboarding-gate.tsx:架构图 shared/components 清单中未列出此组件,但文件仍存在。
  5. 未记录 use-permission.tsauth-session-provider.tsx 在 auth 模块中的归属:被随意划入 shared但语义上属于 auth 模块对外暴露的客户端 API。

二、现存问题与原因分析

2.1 三层架构违规P0

问题 1注册页面 Server Action 内联直接访问数据库

  • 位置register/page.tsx
  • 问题registerAction 直接内联在页面文件中,通过 await import("@/shared/db")await import("@/shared/db/schema") 直接 INSERT users / usersToRoles / roles 三张表,未通过任何 modules/*/data-access。同时文件内重复实现了 calcAgenormalizeBcryptHash 两个纯函数(前者与 register-form.tsx#L24-35 重复,后者与 shared/lib/bcrypt-utils.ts#L15-19 重复)。
  • 违反规则
    • 项目规则"app/ 只能调用 modules/ 的 Server Actions 和 data-access不直接访问数据库"
    • 项目规则"模块标准结构要求 data-access.tsactions.ts 分层"
    • 项目规则"Reusable UI components must be extracted to avoid JSX duplication"(此处是逻辑重复)
  • 后果:注册逻辑无法被单测、无法被 mockDB schema 变更需改页面文件;后续添加邮箱验证、审批流、邀请码等扩展需重写;与 modules/users/data-access.tsgetUserProfile/updateUserProfileById 等同样操作 users 表的函数形成两套并行的写入口,职责割裂。

问题 2src/auth.ts 跨越 modules 边界依赖 settings 模块

  • 位置auth.ts const { verifyTwoFactorForLogin } = await import("@/modules/settings/actions-security")
  • 问题:根目录 auth.ts(属于应用层)通过 dynamic import 直接调用 modules/settings/actions-security.ts 中的 verifyTwoFactorForLogin,且 actions-security.ts"use server" 文件,内部函数默认被认为是 Server Action被当作普通函数调用语义混乱。
  • 违反规则
    • 项目规则"严格三层架构,依赖方向单向:app → modules → shared"——auth.ts 通过 import modules 是合法方向,但 2FA 校验逻辑被嵌入 settings 模块而非 auth 自身或 shared造成"settings 反向服务于 auth"的语义错乱
    • 项目规则"模块间只能通过对方 data-access 通信"——此处直接调用 actions-security.ts,未通过 settings data-access
  • 后果settings 模块重构(如拆分 actions-security会破坏 authverifyTwoFactorForLoginauth.ts 调用导致其内部依赖的 getTwoFactorEnabled/getTotpSecret/getBackupCodesHashed 等函数在登录时被加载bundle 体积无谓增加。

问题 3onboarding 页面绕过统一认证入口

  • 位置onboarding/page.tsx import { auth } from "@/auth"const session = await auth()
  • 问题:项目硬约束明确要求"Authentication must use getAuthContext() and getCurrentStudentUser(); avoid mixing with other auth methods",但 onboarding 页面绕过 shared/lib/auth-guard.getAuthContext(),直接调用 auth() 获取 session。同时仅靠 if (!userId) redirect("/login") 手动处理未登录态,不享受 PermissionDeniedError 统一异常与 DataScope 解析。
  • 违反规则
    • 项目硬约束"Authentication must use getAuthContext() and getCurrentStudentUser()"
    • 项目 memory"Authentication must use getAuthContext() and getCurrentStudentUser(); avoid mixing with other auth methods"
  • 后果:未登录异常未走 route-error.tsx 统一处理JWT session 与 DB 中 users.onboardedAt 不一致时只能靠手动 redirect;与同项目中所有其他 dashboard 页面使用 requireAuth() 的模式不一致,认知成本增加。

问题 4废弃的 OnboardingGate 仍残留在代码库

  • 位置onboarding-gate.tsx313 行)
  • 问题:架构图明确记录"P2-1 已修复:用 middleware 强制重定向替代客户端 Dialog",但 OnboardingGate 文件仍存在且未被任何代码 importgrep OnboardingGate 仅返回定义行)。文件内含 isAdmin/isTeacher/isStudent/isParent 用权限点反推角色的硬编码逻辑:
    const isAdmin = permissions.includes(Permissions.SETTINGS_ADMIN)
    const isTeacher = permissions.includes(Permissions.EXAM_CREATE)
    const isStudent = permissions.includes(Permissions.HOMEWORK_SUBMIT) && !permissions.includes(Permissions.EXAM_CREATE)
    const isParent = !permissions.includes(Permissions.EXAM_CREATE) && !permissions.includes(Permissions.HOMEWORK_SUBMIT) && permissions.includes(Permissions.EXAM_READ)
    
  • 违反规则
    • 项目规则"前端权限判断统一使用 usePermission().hasPermission(),严禁 role === 'xxx' 硬编码"
    • 项目规则"避免 backwards-compatibility 残留"
  • 后果:误用风险(新人可能 import 它);阅读代码困惑;增加了未使用的 313 行代码体积;权限点反推角色的逻辑本身存在漏洞(如同时拥有 EXAM_CREATE 和 HOMEWORK_SUBMIT 会被判定为 teacher

2.2 文件大小与组织P1

问题 5onboarding-stepper.tsx 451 行已接近 500 行上限

  • 位置onboarding-stepper.tsx
  • 问题:单组件混合 4 步骤 UI 渲染、状态管理、子女行动态增删、URL 参数同步、表单提交、跳过逻辑、权限校验。文件中 step === 0/1/2/3 分支硬编码,新增"邮箱验证"、"隐私同意"等步骤需改大组件。
  • 违反规则:项目规则"React 组件 ≤ 500 行(复杂表单可放宽至 800 行)"虽未超出,但已接近上限,且组件未拆分。
  • 后果:单测困难;步骤间复用无法实现;新增步骤需大改。

问题 6auth.ts authorize 回调仍混合 7 类职责

  • 位置auth.ts#L34-L141
  • 问题authorize 回调内联:① 速率限制 ② 用户查询 ③ 账户锁定校验 ④ bcrypt 密码比对 ⑤ 失败计数 ⑥ 2FA 调用(跨模块 import settings ⑦ 角色加载与 resolvePrimaryRole。架构图 §3.2.4 标注"后续可选优化authorize 回调可进一步拆分为 checkRateLimit / checkAccountLockout / verifyPassword / loadUserRoles使 auth.ts 降至 ≤150 行",但实际未拆分。
  • 违反规则:项目规则"Server Actions / Data Access 模块 ≤ 800 行"虽未超,但 authorize 单函数 107 行违反单一职责。
  • 后果难以单测任何修改都可能破坏登录链路rate-limit 改为 Redis 时需改 auth.ts。

2.3 权限缺失与硬编码P0/P1

问题 7注册 Server Action 完全无安全防护

  • 位置register/page.tsx#registerAction
  • 问题
    1. 无速率限制auth.ts authorize 有 rateLimit(RATE_LIMIT_RULES.LOGIN),但 registerAction 完全无限制
    2. 无 Zod 校验:仅 if (!email) / if (password.length < 6) 字符串判断,未使用项目标准的 OnboardingSchema 模式
    3. 无邮箱验证流程:注册即激活,无验证邮件、无管理员审批
    4. 无 CAPTCHA:机器人可批量注册
    5. 无审计日志:注册成功未调用 logLoginEvent({ action: "signup", status: "success" })schema 中 loginLogActionEnum 已包含 "signup",但实际未使用)
    6. 角色查找静默失败并自动创建:第 92-94 行 if (!roleRow) { await db.insert(roles).values({ name: "student" }) }——任何访问 /register 的匿名用户都可触发 roles 表插入,且无 isSystem: true、无 isEnabled: true,污染 RBAC 配置
    7. 密码哈希 cost=10:低于推荐 cost=12未做 breached password 检测
    8. 错误信息泄露数据库细节:第 158-162 行直接返回 创建账户失败:${msg}(含 sqlMessage
  • 违反规则
    • 项目规则"Server Action 必须使用 requirePermission() 进行权限校验"(注册虽无登录态,但应有等效防护)
    • 项目规则"输入使用 Zod 验证,验证失败返回结构化错误"
    • 项目安全规范"JWT/session ID 存储在 httpOnly + Secure + SameSite=Strict 的 Cookie 中"
    • 项目规则"禁止 dangerouslySetInnerHTML"——此处虽未用,但错误消息直接暴露 SQL 错误违反同源安全原则
  • 后果批量注册攻击、roles 表污染、SQL 错误信息泄露、注册行为无法审计与监控。

问题 8proxy.tsresolveDefaultPath 硬编码角色名

  • 位置proxy.ts#L45-L52
    function resolveDefaultPath(roles: string[]): string {
      if (roles.includes("admin")) return "/admin/dashboard"
      if (roles.includes("grade_head") || roles.includes("teaching_head")) return "/teacher/dashboard"
      if (roles.includes("teacher")) return "/teacher/dashboard"
      if (roles.includes("student")) return "/student/dashboard"
      if (roles.includes("parent")) return "/parent/dashboard"
      return "/dashboard"
    }
    
  • 问题:硬编码 6 个角色名。同时 onboarding/data-access.ts#resolveDefaultPathByRoles 重复实现了相同逻辑,两份代码存在同步风险。
  • 违反规则:项目规则"严禁 role === 'xxx' 硬编码"(虽然 proxy 是服务端,但应使用权限点或角色配置驱动)。
  • 后果:新增自定义角色(如"教研员"、"访客")时不会自动路由;与 RBAC 模块动态角色支持不一致;两份实现可能漂移。

问题 9onboarding 完成时角色信息从 JWT 读取但未与 DB 二次校验

  • 位置onboarding/actions.ts#L100-L107 const roleNames = ctx.roles
  • 问题ctx.roles 来自 JWT但 JWT 在用户被撤销角色后到下次 refresh 之间存在窗口期。onboarding 仍按旧角色执行 enrollStudentByInvitationCode / enrollTeacherByInvitationCode / bindParentToChild,可能为已被撤销 student 角色的用户绑定班级。
  • 违反规则:项目规则"Server Action 必须调用 requirePermission() 进行权限校验"——onboarding Action 仅 requireAuth(),未对"执行 student/teacher/parent 角色特定操作"做权限校验。
  • 后果:被撤销角色的用户仍能完成 onboarding 并绑定班级/子女,造成数据污染。

2.4 国际化遗漏P1

问题 10login-form.tsx 完全未使用 i18n硬编码英文

  • 位置login-form.tsx
  • 问题:硬编码 "Welcome back"、"Enter your email to sign in to your account"、"Email"、"Password"、"Forgot password?"、"Invalid email or password"、"Invalid 2FA code"、"Sign In with Email"、"Verify & Sign In"、"Or continue with"、"GitHub"、"Don't have an account?"、"Sign up"。但 shared/i18n/messages/{en,zh-CN}/auth.json 已存在完整翻译键,完全未被使用
  • 违反规则:项目规则"所有用户可见文本必须适配 i18n使用 next-intl提取翻译键"。
  • 后果中英文混杂login-form 全英文register-form 全中文);多语言切换无效;与项目其他模块均使用 useTranslations 的模式不一致。

问题 11register-form.tsx 硬编码中文,未使用 i18n

  • 位置register-form.tsx
  • 问题:硬编码 "创建账户"、"填写以下信息以创建您的账户"、"姓名"、"邮箱"、"密码"、"出生日期"、"当前年龄:{age} 岁"、"检测到您是未成年人,请填写监护人信息"、"监护人姓名"、"监护人电话"、"监护人与您的关系"、"父亲"、"母亲"、"其他法定监护人"、"我已阅读并同意"、"《隐私政策》"、"《用户协议》"、"我确认已获得监护人同意使用本服务"、"创建账户"、"已有账户?"、"立即登录"。
  • 违反规则:同上。
  • 后果英文用户无法理解翻译键已存在但未使用toast 错误消息也硬编码。

问题 12onboarding actions.tsdata-access.ts 错误消息硬编码中文

  • 位置
  • 问题:硬编码 "输入校验失败"、"班级码绑定失败"、"Onboarding 提交失败"、"未找到该邮箱对应的学生"、"学生未设置出生日期,请联系管理员"、"子女生日不匹配"、"子女手机号后 4 位不匹配"。onboarding.json#toast 已有 submitFailedinputInvalid 等键,但 Server Action 未通过 getTranslations 读取。
  • 违反规则:项目规则"所有用户可见文本必须适配 i18n"。
  • 后果:英文 locale 下 toast 显示中文;与项目其他模块 Server Action 已迁移至 i18n 的模式不一致。

问题 13(auth) 路由组的 error.tsxnot-found.tsx 静态英文

  • 位置
  • 问题:硬编码 "Authentication Error"、"There was a problem signing you in. Please try again."、"Try again"、"Page Not Found"、"The authentication page you are looking for does not exist."、"Return to Login"。
  • 违反规则:项目规则"所有用户可见文本必须适配 i18n"。
  • 后果:与 (auth) 路由组下的 login/register混合中英文语言不一致。

问题 14privacy/page.tsxterms/page.tsx 静态中文

  • 位置
  • 问题:整页隐私政策与用户协议硬编码中文,未提取 i18n 键。
  • 违反规则:项目规则"所有用户可见文本必须适配 i18n"。
  • 后果:英文 locale 下展示中文;维护政策文本需改代码而非改 JSON。

问题 15AuthLayout 左侧品牌 testimonial 硬编码英文且与 K12 场景不符

  • 位置auth-layout.tsx#L19-L22
  • 问题
    <blockquote className="space-y-2">
      <p className="text-lg">
        &ldquo;This platform has completely transformed how we deliver education to our students. The attention to detail and performance is unmatched.&rdquo;
      </p>
      <footer className="text-sm">Sofia Davis</footer>
    </blockquote>
    
    硬编码英文 testimonial 与虚构人物 "Sofia Davis",与 K12 教育系统定位不符。
  • 违反规则:项目规则"所有用户可见文本必须适配 i18n"。
  • 后果i18n 失效;学校白标部署无法配置 testimonial与产品定位不符。

2.5 错误处理与边界P1

问题 16LoginForm 未区分账户锁定、速率限制、2FA 缺失等错误

  • 位置login-form.tsx#L62-L69
  • 问题:所有失败统一显示 "Invalid email or password" 或 "Invalid 2FA code"。signIn("credentials", { redirect: false }) 返回的 result.error 是字符串(如 "CredentialsSignin"),代码仅判断 !result?.error,未针对不同 error code 给出差异化提示。
  • 违反规则:项目规则"明确处理空数据、无权限、网络异常等边界状态"。
  • 后果合法用户被锁定后不知如何解锁攻击者无法感知是否成功2FA 缺失与密码错误提示混淆。

问题 17LoginFormpreflightTwoFactorAction 失败时静默降级

  • 位置login-form.tsx#L44-L46
    try {
      const preflight = await preflightTwoFactorAction(email)
      ...
    } catch {
      // 预检失败时静默降级为普通登录
    }
    
  • 问题2FA 预检失败时降级为普通登录,但用户可能已启用 2FA会被 authorize 拒绝,用户需重复输入密码。
  • 违反规则:项目规则"明确处理空数据、无权限、网络异常等边界状态"。
  • 后果:用户体验差;暴露 2FA 状态(攻击者可通过预检失败推断用户启用 2FA

问题 18(auth) 路由组无 loading.tsx

  • 位置src/app/(auth)/(缺文件)
  • 问题:缺少 loading.tsx,违反项目硬约束"所有 student 路由必须包含 loading.tsx 和 error.tsx"(虽约束明确为 student 路由,但 (auth) 应同样对待)。
  • 违反规则:项目硬约束。
  • 后果:路由切换时无骨架屏;与 (dashboard) 路由组模式不一致。

问题 19LoginFormRegisterForm 无 React Error Boundary 包裹

  • 位置login/page.tsxregister/page.tsx
  • 问题:仅 <Suspense fallback={null}> 包裹 LoginForm无 Error Boundary如果组件抛错会冒泡到 error.tsx 全页错误,用户丢失已输入数据。
  • 违反规则:项目规则"每个独立的数据区块必须用 React Error Boundary 包裹"。
  • 后果:单个组件错误导致整页不可用。

问题 20proxy.ts 重定向时未携带原始路径到 onboarding 完成后回跳

  • 位置proxy.ts#L84-L90
  • 问题:未登录用户访问 /teacher/classes 时被重定向到 /login?callbackUrl=...,但登录成功后若 onboarding 未完成又被重定向到 /onboardingonboarding 完成后又跳转到默认路径 /teacher/dashboard,丢失了用户原始意图 /teacher/classes
  • 违反规则:项目规则"URL parameters (e.g., classId) must be explicitly read and handled in target pages to prevent broken navigation"。
  • 后果:用户体验割裂,需重新导航到目标页面。

2.6 类型不安全P0/P1

问题 21auth.ts 多处 as 断言

  • 位置
    • auth.ts#L146 const u = user as { id: string; role?: string; roles?: string[]; name?: string }
    • auth.ts#L196 session.user.permissions = (token.permissions ?? []) as typeof token.permissions
    • auth.ts#L217-L223 (message as { userId?: string })?.userId ?? (message as { token?: { id?: string } })?.token?.id ?? ""
  • 问题:使用 as 断言而非类型守卫。next-auth.d.ts 已声明 Session/JWT 类型,但 jwt callback 中 user 参数是 anyNextAuth 类型缺陷),需用类型守卫。
  • 违反规则:项目规则"禁止 as 断言(除类型收窄外)"。
  • 后果:类型与运行时不一致;重构易引入 bugjwt callback 中 token.permissions 类型与 Permission[] 不兼容。

问题 22proxy.ts(token.roles as string[]) 断言

  • 位置proxy.ts#L93,99
  • 问题:直接 as string[],未做类型守卫;旧 token 数据格式异常时(如 token 为 v1 格式无 roles 字段)会 runtime error。
  • 违反规则:项目规则"禁止 as 断言"。
  • 后果:旧 token 用户访问时崩溃。

问题 23register-form.tsx ActionState 未指定泛型

  • 位置register-form.tsx#L20,38
  • 问题import type { ActionState } from "@/shared/types/action-state"registerAction: (formData: FormData) => Promise<ActionState> 未指定泛型参数 Tdata 字段类型为 unknown
  • 违反规则:项目规则"函数返回值必须显式标注"。
  • 后果success 分支访问 res.data 时无类型提示。

2.7 单测不可行P0

问题 24auth 模块几乎无单元测试e2e 测试与 UI 不一致

  • 位置tests/e2e/smoke-auth.spec.ts
  • 问题
    1. rate-limit.tspassword-policy.tsrole-utils.tsbcrypt-utils.tshttp-utils.tspassword-security-service.tssession.tsauth-guard.tspermissions.tsresolvePermissions)均无对应 .test.ts
    2. auth.ts 的 authorize/jwt/session 回调无测试
    3. proxy.ts 的路由权限判断无测试
    4. e2e 测试期望 getByLabel("Full Name")button { name: "Create Account" }(英文),但 register-form.tsx 实际是中文"姓名"和"创建账户"——e2e 必然失败
  • 违反规则:项目规则"数据获取、计算、格式化等纯逻辑全部放入纯函数或 hooks与 UI 分离;导出清晰的接口类型以便 mock"。
  • 后果重构无回归保障rate-limit 改为 Redis 时无对照基线e2e 在 CI 失败或被 skip。

2.8 组件不可复用P1

问题 25LoginForm / RegisterForm 无法被复用与测试

  • 位置login-form.tsxregister-form.tsx
  • 问题
    • LoginForm 硬编码 signIn("credentials"),未抽象 AuthService 接口,与 next-auth/react 紧耦合
    • RegisterForm 接受 registerAction props 但未定义数据契约接口(Promise<ActionState> 无泛型)
    • 两个组件均直接 import @/modules/settings/actions-securitylogin-form和通过 props 注入register-form方式不统一
    • GitHub 登录按钮是死链无 onClick
  • 违反规则:项目规则"完全解耦:通过定义 TypeScript 接口抽象数据依赖,使用 React Context 注入数据服务"。
  • 后果:换 SSO 提供商需重写组件;无法在测试中 mock多角色场景下无法复用。

问题 26AuthLayout 静态硬编码品牌信息

  • 位置auth-layout.tsx#L12-L22
  • 问题GraduationCap 图标、"Next_Edu" 品牌名、"Sofia Davis" testimonial 全部硬编码,无 props 注入。
  • 违反规则:项目规则"最大化复用:识别四个角色共用的 UI 块和业务逻辑块,抽象为泛型组件和 hooks"。
  • 后果:学校白标部署无法配置 logo/标语;无法在 admin 设置页预览登录页样式。

2.9 安全性P0/P1

问题 27rate-limit.ts 是单实例内存实现

  • 位置rate-limit.ts#L1-L9
    /**
     * In-memory rate limiter (single-instance only).
     * For multi-instance deployments, replace with @upstash/ratelimit + @upstash/redis.
     */
    const rateLimitMap = new Map<string, RateLimitEntry>()
    
  • 问题:注释明确说"For multi-instance deployments, replace with @upstash/ratelimit + @upstash/redis",但当前是 Map 单实例。
  • 违反规则:项目规则"安全性:所有敏感数据查询必须在 data-access 层结合当前用户权限过滤"。
  • 后果多实例部署时速率限制失效攻击者可分散请求绕过K8s 水平扩容后暴力破解防护失效。

问题 28JWT 中存储 permissions 数组导致 token 体积过大

  • 位置auth.ts#L153,184 token.permissions = await resolvePermissions(allRoles)
  • 问题67 个权限点字符串数组写入 JWTadmin 角色约 67 个 × 平均 20 字符 = ~1.3KBJWT cookie 体积可能超过 4KB 限制;权限变更需用户重新登录或 JWT refresh 才能生效。
  • 违反规则:项目规则"JWT/session ID 存储在 httpOnly + Secure + SameSite=Strict 的 Cookie 中"——cookie 体积过大可能被浏览器截断。
  • 后果cookie 体积超限导致 session 丢失权限撤销有延迟admin 角色拥有最多权限,最易触发。

问题 29onboarding/data-access.ts bindParentToChild 未做独立速率限制

  • 位置onboarding/data-access.ts#bindParentToChild
  • 问题:家长绑定子女的三因子验证(生日 365 × 手机后4 10000 = 3.65M)虽然组合空间大,但单次 onboarding Action 内可循环调用 10 次(children.length ≤ 10),无独立速率限制;被撤销子女关系的家长可反复尝试枚举绑定。
  • 违反规则:项目安全规范。
  • 后果枚举攻击窗口audit_logs 仅记录成功,未记录失败次数。

问题 30sessions 表存在但 JWT 策略下未使用,远程登出功能形同虚设

  • 位置
  • 问题NextAuth 配置 JWT 策略,但 schema 中 sessions 表仍存在(无数据);revokeAllOtherSessionsAction 删 sessions 表但实际无活跃记录,注释承认 "JWT-based, no active DB sessions"。
  • 违反规则:项目规则"避免 backwards-compatibility 残留"。
  • 后果用户误以为可远程登出其他设备sessions 表 schema 与运行时不一致,增加困惑。

问题 31proxy.ts 未对 /admin/* 子路径做细粒度权限校验

  • 位置proxy.ts#L13-L19
  • 问题/admin 前缀匹配 school:manage 即放行所有 admin 子路径。/admin/ai-settings/admin/roles/admin/permissions 已用 SPECIFIC_ROUTE_PERMISSIONS 例外处理,但 /admin/announcements/admin/audit-logs/admin/elective/admin/questions/admin/school/*/admin/users 等仍依赖页面内 requirePermission 兜底。
  • 违反规则:项目规则"安全性:所有敏感数据查询必须在 data-access 层结合当前用户权限过滤Server Action 二次校验"——proxy 应作为第一道防线。
  • 后果:页面若忘记调用 requirePermission 即造成越权;与"/teacher/parent 已使用角色独有权限点"的设计不一致。

2.10 a11y 与性能P1

问题 32LoginForm 无 ARIA 错误关联、无"显示密码"按钮、无 Caps Lock 检测

  • 位置login-form.tsx#L89,111-117,156-158
  • 问题
    1. 错误 <p className="text-sm text-red-600">{error}</p> 未使用 role="alert"、未 aria-describedby 关联到输入框
    2. 密码输入框无显示/隐藏切换
    3. 无 Caps Lock 检测提示
    4. GitHub 登录按钮无 onClick 是死链
  • 违反规则:项目规则"可访问性a11y语义化标签、ARIA 属性、键盘导航"。
  • 后果:屏幕阅读器用户无法感知错误;违反 WCAG 2.2 Target Size 与 Focus Not Obscured。

问题 33(auth) 路由组无 loading.tsx、无骨架屏

  • 位置src/app/(auth)/(缺文件)
  • 问题路由切换无骨架屏login-form 内部用 isLoading state 控制按钮禁用,但无整体加载态。
  • 违反规则:项目规则"异步数据使用 React Suspense + 骨架屏"。
  • 后果:网络慢时用户体验差。

2.11 监控埋点P1

问题 34auth 模块未定义任何 trackEvent 事件

  • 位置track-event.ts#EventName
  • 问题EventName 类型枚举中无 auth.signinauth.signoutauth.signupauth.2fa_enabledauth.password_resetauth.account_lockedauth.rate_limited 等。
  • 违反规则:项目规则"监控:方案中预留关键操作埋点接口"。
  • 后果登录成功率、2FA 启用率、账户锁定触发率等核心指标无法统计;无法接 Sentry/PostHog。

问题 35auth.ts events.signIn/signOut 仅写 login_logs未触发业务埋点

  • 位置auth.ts#L205-L232
  • 问题:登录成功仅 logLoginEvent,未 void trackEvent({ name: "auth.signin", ... })
  • 违反规则:项目规则"监控:方案中预留关键操作埋点接口"。
  • 后果:监控仪表盘缺失;无法告警异常登录地理/设备。

2.12 可扩展性P1

问题 36proxy.ts 路由权限配置硬编码,无配置驱动

  • 位置proxy.ts#L13-L43
  • 问题ROUTE_PERMISSIONSDASHBOARD_ROUTE_PERMISSIONSSPECIFIC_ROUTE_PERMISSIONSAPI_PERMISSIONS 4 个常量写死在文件中,新增角色或权限点需改代码。
  • 违反规则:项目规则"可扩展性:采用配置驱动设计"。
  • 后果:与 RBAC 模块的 DATA_SCOPE_RULES 配置驱动不一致;新增路由时易遗漏权限配置。

问题 37onboarding 流程不支持配置驱动

  • 位置onboarding-stepper.tsx
  • 问题:步骤硬编码 4 步roleConfirm/basicInfo/roleInfo/complete新增"邮箱验证"、"隐私同意"、"家长扫码绑定子女"等步骤需改组件代码。
  • 违反规则:项目规则"可扩展性:采用配置驱动设计,例如通过角色配置决定该模块渲染哪些 Widget/子模块"。
  • 后果:扩展性差;与项目其他模块(如 dashboard widget-configs.ts的配置驱动模式不一致。

2.13 数据库与 Schema 一致性P2

问题 38users 表混合了认证、用户资料、未成年人保护等多类字段

  • 位置schema.ts#L27-L57
  • 问题users 表 31 个字段混合认证password, emailVerified, image、用户资料name, phone, address, gender, age、未成年人保护birthDate, guardianName, guardianPhone, guardianRelation, consentAcceptedAt、组织gradeId, departmentId、onboardingonboardedAt
  • 违反规则:项目规则"模块标准结构要求 schema 按职责拆分"。
  • 后果schema 文件 1111 行(架构图 P0-1 已记录);认证模块修改 password 字段时影响用户资料模块;难以按表分库分表。

三、行业差距对比

3.1 K12 认证主流模式对标

维度 行业最佳实践 本项目现状 差距
账号创建 管理员预分配 + 开放注册关闭PowerSchool、Veracross、Clever 开放注册,任何人可注册 student 账号 重大差距:未实现邀请码/批量导入预分配;任何人可污染 student 角色池
SSO 单点登录 Clever SSO2750 万师生)/ SAML/OIDC / Clever Badges低年级二维码徽章 仅 Credentials Provider无 SSO/SAML/OIDC 重大差距:未对接 Google Workspace / Microsoft Entra ID / ClassLink无法满足大型学区统一身份需求
家长-学生绑定 PowerSchool Access ID + Access Password 双因子凭据(校方发放) 邮箱 + 生日 + 手机后4 三因子(学生已有信息) 中等差距:组合空间 3.65M 已接近,但因子均来自学生已有数据,校方未发放独立绑定码;缺少学校发放的 Access Code 表
邮箱验证 注册后强制邮箱验证 + 接受条款 注册即激活,无邮箱验证 重大差距:违反 PowerSchool/Veracross 标准
Breached Password 检测 Auth0 Breached Passwords 比对已知泄露库 仅 bcrypt cost=10 重大差距:未接入 HaveIBeenPwned API 或 k-anonymity 范围查询
2FA 覆盖 TOTP + 备用码 + 推送 + 短信 + WebAuthn 多因子 TOTP + 备用码 良好TOTP+备用码已实现,符合主流;可扩展 WebAuthn
未成年人保护 COPPA/FERPA 声明 + 监护人同意书 隐私政策/用户协议页 + 注册时勾选 良好:已实现未成年人监护人字段;但缺少监护人电子签名/同意书记录

3.2 UI/UX 差距

维度 行业最佳实践 本项目现状 差距
品牌展示 PowerSchool District Branding / Auth0 Universal Login 自定义域+模板 AuthLayout 硬编码 Next_Edu + Sofia Davis testimonial 差距:无学校 logo/配色/标语自定义;无 admin 端品牌配置入口
多角色切换 UI Auth0 Actions 在 Token 中下发多角色,前端 role-switcher 下拉 已实现layout/app-sidebar.tsx 多角色切换) 达标
Onboarding 步骤 3-5 步可跳过式 + 进度持久化到 localStorage/Cookie 4 步可跳过 + URL query 持久化 达标URL query 比 localStorage 更优
错误反馈 Auth0 区分"用户不存在/密码错/账户锁定/速率限制/2FA 缺失" 统一 "Invalid email or password" 差距:违反 NIST SP 800-63-4 "明确错误反馈"建议;用户体验差
显示密码按钮 WCAG 2.2 Target Size ≥24×24 + 可见焦点环 差距:违反 WCAG 2.2
Caps Lock 检测 主流浏览器原生支持 + UI 提示 差距

3.3 安全合规差距

维度 行业最佳实践 本项目现状 差距
账户锁定 NIST SP 800-63-4 risk-based throttling5-10 次失败后临时锁定 15-30 分钟 5 次失败后锁定 30 分钟PASSWORD_RULES 达标
速率限制 按 IP + 账号双维度,登录端点 ≥10 req/min + 指数退避 5 attempts / 15 minIP+email单实例内存 差距:未实现 Redis 分布式阈值偏严5 次/15min 可能误锁合法用户)
密码策略 NIST 800-63-4 取消强制复杂度与定期轮换,最低 8 位,强制 breached password 检查 8 位 + 大小写 + 数字(无特殊字符要求) 部分达标:长度符合;未做 breached password 检查;强制复杂度与 NIST 最新建议有偏差
审计日志保留 CoSN K-12 CVAT + CIS 2025 K-12 报告建议 ≥1 年FERPA 要求可审计 login_logs 表存在但无保留期配置 差距无保留期清理任务audit 模块已有 retention.ts但 login_logs 未接入)
跨角色越权 服务端强制 RBAC/ABAC禁止仅前端隐藏 proxy.ts + requirePermission 双重校验 达标
Sessions 远程登出 JWT 黑名单或短期 token + refresh token sessions 表存在但 JWT 策略下未使用 差距:远程登出形同虚设

3.4 a11y 与 i18n 差距

维度 行业最佳实践 本项目现状 差距
WCAG 2.2 Focus Not Obscured (AA)、Target Size ≥24×24、Consistent Help (A)、Dragging Movements (AA) LoginForm 无 ARIA 错误关联、无显示密码按钮、无 Caps Lock 检测 差距
ARIA aria-labelaria-invalidaria-describedbyrole="alert" LoginForm 错误 p 标签无 role="alert" 差距
键盘导航 完整 Tab 序、可见焦点环(对比度 ≥3:1 基本支持,但未显式验证 部分达标
i18n PowerSchool 支持 EN/ES/ZHURL 持久化 auth.json/onboarding.json 存在但 LoginForm/RegisterForm 未使用 重大差距i18n 翻译键已准备但完全未接入

3.5 可观测性差距

维度 行业最佳实践 本项目现状 差距
核心指标 登录成功率、首次登录失败原因分布、2FA 启用率、密码重置率、锁定触发率、SSO 同步失败率 login_logs 表记录原始事件,但无聚合仪表盘 差距:无仪表盘;无埋点
异常告警 异常登录地理/设备、暴力破解阈值触发、breached password 命中激增;接入 SIEM 重大差距
审计日志切片 按学区/学校/角色维度切片 仅按 userId/userEmail 差距

四、改进优先级建议

P0紧急影响安全/架构合规)

# 问题 改进方向
P0-1 注册 Server Action 直接访问 DB问题 1 新建 modules/auth/data-access.tsmodules/auth/actions.ts,将 registerAction 迁移至 actions.ts 并通过 data-access 访问 users 表;删除页面内联 registerAction 与重复的 calcAge/normalizeBcryptHash
P0-2 auth.ts 跨模块依赖 settings问题 2 将 2FA 校验逻辑从 modules/settings/actions-security.ts 抽取到 modules/auth/services/two-factor-service.ts(纯函数 + 通过 data-access 访问 settings 表auth.ts 改为 await import("@/modules/auth/services/two-factor-service")
P0-3 onboarding 页面绕过统一认证入口(问题 3 onboarding/page.tsx 使用 getAuthContext() 替代直接 auth(),未登录抛 PermissionDeniedErrorroute-error.tsx 处理
P0-4 废弃 OnboardingGate 残留(问题 4 删除 src/shared/components/onboarding-gate.tsx
P0-5 注册接口无安全防护(问题 7 modules/auth/actions.ts 中:① 添加 registerRateLimit(独立于 LOGIN② 用 RegisterSchema Zod 校验;③ 注册成功调用 logLoginEvent({ action: "signup" });④ 删除"自动创建 student 角色"逻辑,改为查找失败时返回错误;⑤ 错误消息不暴露 SQL 细节;⑥ 接入 CAPTCHAhCaptcha 或 Cloudflare Turnstile
P0-6 auth.ts 多处 as 断言(问题 21 用类型守卫替代:function isAuthUser(u: unknown): u is { id: string; role?: string; ... }jwt callback 中 token 类型已声明,无需断言
P0-7 e2e 测试与 UI 不一致(问题 24 重写 tests/e2e/smoke-auth.spec.ts,使用 i18n 后的翻译键或 data-testid 定位;同时为 rate-limit.ts/password-policy.ts/role-utils.ts/bcrypt-utils.ts 添加 .test.ts 单元测试
P0-8 proxy.ts resolveDefaultPath 硬编码角色(问题 8 抽取 shared/lib/route-resolver.ts,按权限点优先 + 角色兜底解析默认路径;proxy.tsonboarding/data-access.ts 共用一份实现

P1重要影响体验/可维护性)

# 问题 改进方向
P1-1 LoginForm/RegisterForm 未使用 i18n问题 10/11/12/13/14/15 接入 useTranslations("auth") / useTranslations("onboarding")Server Action 通过 getTranslations 读取错误消息;扩展 auth.json 增加 errors.accountLocked/errors.rateLimited/errors.twoFactorRequired 等键
P1-2 onboarding-stepper 拆分(问题 5 拆为 step-role-confirm.tsx/step-basic-info.tsx/step-role-info.tsx/step-complete.tsx 4 个子组件 + useOnboardingForm hook 管理状态;主组件仅做步骤编排
P1-3 auth.ts authorize 回调拆分(问题 6 拆为 checkRateLimit/checkAccountLockout/verifyPassword/verifyTwoFactor/loadUserRoles 5 个纯函数 + serviceauthorize 仅编排
P1-4 LoginForm 错误反馈细化(问题 16/17/32 区分 error codeCredentialsSignin/CredentialsSignout/OAuthCallbackError);添加 ARIA role="alert"aria-describedby添加显示密码按钮、Caps Lock 检测
P1-5 (auth) 路由组添加 loading.tsx + Error Boundary问题 18/19/33 新建 app/(auth)/loading.tsx 骨架屏;用 shared/components/widget-boundary.tsx 包裹 LoginForm/RegisterForm
P1-6 rate-limit 改为分布式(问题 27 抽象 RateLimiter 接口,默认内存实现,可注入 Redis 实现;环境变量 RATE_LIMIT_DRIVER=memory|redis 切换
P1-7 JWT 体积优化(问题 28 方案 AJWT 仅存 userId + rolespermissions 每次请求通过 getAuthContext() 从 DB/缓存查询(牺牲少量性能换 token 体积);方案 BJWT 存 permissions 哈希DB 中按哈希缓存权限集
P1-8 bindParentToChild 速率限制(问题 29 modules/onboarding/actions.ts 中针对 children.length 添加独立速率限制 key onboarding:bind:{parentId},限制每小时 5 次
P1-9 sessions 表清理(问题 30 删除 schema.ts 中 sessions 表定义 + revokeAllOtherSessionsAction 改为基于 JWT 短期 token + refresh token 黑名单;或在 auth.ts 中切换为 strategy: "database"(不推荐,性能差)
P1-10 proxy.ts 细粒度权限(问题 31 扩展 SPECIFIC_ROUTE_PERMISSIONS 覆盖 /admin/announcements//admin/audit-logs//admin/users 等子路径;或改为基于 RBAC 模块的 ROUTE_PERMISSION_CONFIG 配置驱动
P1-11 trackEvent 接入 auth 事件(问题 34/35 EventName 中新增 auth.signin_success/auth.signin_failure/auth.signout/auth.signup/auth.2fa_enabled/auth.account_locked/auth.rate_limitedauth.ts events.signIn/signOut 与 authorize 失败分支调用 void trackEvent(...)
P1-12 proxy.ts 配置驱动(问题 36 抽取 shared/lib/route-permissions.ts,导出 ROUTE_PERMISSIONS 等常量proxy.ts 从此文件 import
P1-13 onboarding 配置驱动(问题 37 定义 OnboardingStepConfig 类型 + ONBOARDING_STEPS 数组根据角色过滤步骤onboarding-stepper 仅渲染配置

P2中长期影响可扩展性/合规)

# 问题 改进方向
P2-1 users 表字段混合(问题 38 长期拆分:users认证核心id/email/password/emailVerified+ user_profiles资料name/phone/address/gender/age+ user_minor_protection(监护人信息)+ user_onboardingonboardedAt通过 view 或 data-access 层聚合
P2-2 引入 SSO/SAML/OIDC 接入 NextAuth 的 GoogleProvider/MicrosoftEntraIDProvider/SAMLProvideradmin 端配置 IdP 元数据
P2-3 引入邀请码预分配机制 新增 invitation_codescode/email/role/classId/createdAt/expiresAt/usedByadmin 批量生成邀请码替代开放注册
P2-4 Breached Password 检测 接入 haveibeenpwned APIk-anonymity 范围查询);注册/改密时校验
P2-5 登录审计日志保留期 复用 audit 模块的 retention.ts,为 login_logs 配置 1 年保留期;添加定时任务清理
P2-6 AuthLayout 品牌配置 admin 端新增"学校品牌"配置logo/配色/标语/testimonialAuthLayout 通过 props 注入
P2-7 监控仪表盘 新增 admin/monitoring 页面展示登录成功率/2FA 启用率/锁定触发率;接入 Sentry/PostHog
P2-8 WebAuthn 支持 接入 @simplewebauthn/server2FA 设置页新增 WebAuthn 选项

五、架构图同步说明

本次审计发现架构图(004_architecture_impact_map.md005_architecture_data.json)需补充/修改以下节点:

5.1 需新增节点

  1. modules/onboarding 作为独立模块章节§2.X导出 getOnboardingStatusAction/completeOnboardingAction/OnboardingSchema,依赖 shared/* + modules/classes/data-access + modules/rbac/lib/data-scope-resolver(间接)。
  2. shared/components/onboarding-gate.tsx 加入 shared/components 清单,标记为"已废弃,待删除"。
  3. shared/lib/route-resolver.ts(重构后新增)作为 proxy.ts 与 onboarding/data-access.ts 共用的纯函数。

5.2 需修改节点

  1. §3 根目录 auth.ts
    • 依赖列表新增 modules/settings/actions-security(当前遗漏)
    • 标注"重构后将改为依赖 modules/auth/services/two-factor-service"
  2. §2.24 modules/auth
    • 文件清单新增 data-access.tsactions.tsschema.tstypes.tsservices/two-factor-service.ts(重构后)
    • 删除"纯 UI 模块,无 data-access/actions/types"描述
  3. §3 proxy.ts
    • 导出函数列表新增 resolveDefaultPath(当前遗漏)
    • 依赖列表新增 shared/lib/route-resolver(重构后)
  4. shared/lib 清单
    • 新增 route-permissions.tsroute-resolver.ts(重构后)

5.3 需删除节点

  1. shared/components/onboarding-gate.tsx 标记为"待删除P0-4",重构后从清单移除。
  2. app/api/onboarding/status/route.tsapp/api/onboarding/complete/route.ts 标记为"已废弃410 Gone",重构后可删除。

六、重构方案设计

6.1 完全解耦AuthService 接口抽象与依赖注入

6.1.1 模块边界重新划分

src/modules/auth/
├─ actions.ts                    # Server ActionsregisterAction / loginRateLimitedAction
├─ data-access.ts                # data-accesscreateUser / getUserByEmail / 等)
├─ schema.ts                     # ZodRegisterSchema / LoginSchema
├─ types.ts                      # 类型AuthService interface / RegisterInput / 等)
├─ services/
│  ├─ two-factor-service.ts     # 2FA 校验纯函数(供 auth.ts 调用)
│  ├─ password-service.ts        # 密码哈希纯函数 + breached password 检测
│  └─ rate-limit-service.ts      # 速率限制抽象memory / redis 双实现)
├─ context/
│  └─ auth-service-provider.tsx  # 客户端 Context 注入 AuthService
├─ hooks/
│  ├─ use-login-form.ts          # 登录表单状态机
│  └─ use-register-form.ts       # 注册表单状态机
├─ components/
│  ├─ auth-layout.tsx            # 布局(接 brand props
│  ├─ login-form.tsx             # 登录表单(接 AuthService 注入)
│  ├─ register-form.tsx          # 注册表单(接 RegisterService 注入)
│  ├─ two-factor-input.tsx       # 2FA 验证码输入组件(可复用)
│  ├─ password-input.tsx         # 密码输入 + 显示切换 + Caps Lock 检测(可复用)
│  ├─ oauth-buttons.tsx           # OAuth 按钮组GitHub/Google/Microsoft
│  ├─ brand-panel.tsx            # 品牌左侧面板(接 brand 配置)
│  └─ auth-error-boundary.tsx    # 认证错误边界
└─ config/
   └─ brand.ts                   # 默认品牌配置(可被 admin 覆盖)

6.1.2 AuthService 接口定义

// src/modules/auth/types.ts
export interface AuthService {
  /** 登录预检:是否需要 2FA */
  preflightTwoFactor(email: string): Promise<{ required: boolean }>
  /** 执行登录 */
  signIn(input: LoginInput): Promise<SignInResult>
  /** 执行登出 */
  signOut(): Promise<void>
  /** 获取当前 session */
  getSession(): Promise<AppSession | null>
}

export interface RegisterService {
  /** 注册新用户 */
  registerUser(input: RegisterInput): Promise<ActionState<RegisterResult>>
  /** 检查邮箱是否已注册 */
  checkEmailAvailable(email: string): Promise<boolean>
}

// 默认实现NextAuth adapter
export const createNextAuthService = (): AuthService => ({ ... })

6.1.3 客户端 Context 注入

// src/modules/auth/context/auth-service-provider.tsx
"use client"
const AuthServiceContext = createContext<AuthService | null>(null)

export function AuthServiceProvider({ service, children }: {
  service: AuthService
  children: React.ReactNode
}) {
  return <AuthServiceContext.Provider value={service}>{children}</AuthServiceContext.Provider>
}

export function useAuthService(): AuthService {
  const svc = useContext(AuthServiceContext)
  if (!svc) throw new Error("useAuthService must be used within AuthServiceProvider")
  return svc
}

页面使用:

// app/(auth)/login/page.tsx
import { createNextAuthService } from "@/modules/auth/services/next-auth-service"
import { AuthServiceProvider } from "@/modules/auth/context/auth-service-provider"
import { LoginForm } from "@/modules/auth/components/login-form"

const authService = createNextAuthService()

export default function LoginPage() {
  return (
    <AuthServiceProvider service={authService}>
      <ErrorBoundary>
        <LoginForm />
      </ErrorBoundary>
    </AuthServiceProvider>
  )
}

收益LoginForm 不再直接 import next-auth/react,可被测试 mock未来切换 SSO 提供商只需替换 service 实现;不同学校可注入不同 brand 配置。

6.2 组合优先:步骤组件拆分与 hooks 复用

6.2.1 onboarding-stepper 拆分

// src/modules/onboarding/components/onboarding-stepper.tsx重构后
export function OnboardingStepper({ initialStatus }: OnboardingStepperProps) {
  const { step, goToStep } = useOnboardingStep()
  const form = useOnboardingForm(initialStatus)

  return (
    <OnboardingStepContainer step={step} total={form.stepKeys.length}>
      <OnboardingStepRoleConfirm v-if={step === 0} role={form.primaryRole} />
      <OnboardingStepBasicInfo v-if={step === 1} form={form} />
      <OnboardingStepRoleInfo v-if={step === 2} form={form} />
      <OnboardingStepComplete v-if={step === form.maxStep} />
      <OnboardingStepFooter
        step={step}
        maxStep={form.maxStep}
        canNext={form.canNext}
        canSkip={form.canSkip}
        onBack={() => goToStep(step - 1)}
        onNext={() => goToStep(step + 1)}
        onFinish={form.submit}
      />
    </OnboardingStepContainer>
  )
}

6.2.2 共享 hooks

// src/modules/auth/hooks/use-login-form.ts
export function useLoginForm() {
  const [isLoading, setIsLoading] = useState(false)
  const [requiresTwoFactor, setRequiresTwoFactor] = useState(false)
  const [error, setError] = useState<string | null>(null)
  const authService = useAuthService()
  // ... 状态机逻辑
  return { isLoading, requiresTwoFactor, error, submit }
}

6.3 国际化就绪:翻译文件结构示例

6.3.1 auth.json 扩展

{
  "login": {
    "title": "登录",
    "subtitle": "使用您的账号登录 Next_Edu",
    "email": "邮箱",
    "password": "密码",
    "rememberMe": "记住我",
    "signIn": "登录",
    "signingIn": "登录中...",
    "forgotPassword": "忘记密码?",
    "noAccount": "还没有账号?",
    "register": "注册",
    "twoFactor": {
      "title": "请输入二次验证码",
      "subtitle": "输入认证器应用中的 6 位数字",
      "placeholder": "123456",
      "hint": "请输入 6 位认证器代码或 8 位备份码",
      "back": "返回登录",
      "verify": "验证并登录"
    }
  },
  "register": {
    "title": "创建账户",
    "subtitle": "填写以下信息以创建您的账户",
    "name": "姓名",
    "email": "邮箱",
    "password": "密码",
    "birthDate": "出生日期",
    "currentAge": "当前年龄:{age} 岁",
    "minorProtection": {
      "title": "检测到您是未成年人,请填写监护人信息",
      "guardianName": "监护人姓名",
      "guardianPhone": "监护人电话",
      "guardianRelation": "监护人与您的关系",
      "relations": {
        "father": "父亲",
        "mother": "母亲",
        "other": "其他法定监护人"
      },
      "consent": "我确认已获得监护人同意使用本服务"
    },
    "consent": {
      "agree": "我已阅读并同意",
      "privacy": "《隐私政策》",
      "terms": "《用户协议》"
    },
    "signUp": "创建账户",
    "hasAccount": "已有账号?",
    "signIn": "登录"
  },
  "errors": {
    "invalidCredentials": "邮箱或密码错误",
    "emailExists": "该邮箱已被注册",
    "passwordTooWeak": "密码强度不足",
    "accountLocked": "账户已锁定,请 {minutes} 分钟后重试",
    "rateLimited": "登录尝试过于频繁,请稍后再试",
    "twoFactorRequired": "需要二次验证",
    "twoFactorInvalid": "二次验证码错误",
    "twoFactorBackupConsumed": "已使用备份码,剩余 {remaining} 个",
    "network": "网络异常,请稍后重试",
    "unknown": "未知错误,请稍后重试"
  },
  "layout": {
    "brandName": "Next_Edu",
    "testimonial": {
      "quote": "这款平台彻底改变了我们为学生提供教育的方式。对细节的关注和性能无与伦比。",
      "author": "Sofia Davis"
    },
    "orContinueWith": "或使用以下方式登录"
  },
  "error": {
    "title": "认证错误",
    "description": "登录时出现问题,请重试",
    "retry": "重试"
  },
  "notFound": {
    "title": "页面未找到",
    "description": "您查找的认证页面不存在",
    "backToLogin": "返回登录"
  }
}

6.3.2 Server Action i18n

// src/modules/auth/actions.ts
"use server"
import { getTranslations } from "next-intl/server"

export async function registerAction(
  prevState: ActionState<RegisterResult> | null,
  formData: FormData
): Promise<ActionState<RegisterResult>> {
  const t = await getTranslations("auth")
  // ... 使用 t("errors.emailExists") 等
}

6.4 最大化复用:四个角色共用 UI 块

复用单元 复用范围 实现方式
PasswordInput LoginForm / RegisterForm / Settings 修改密码 独立组件 + usePasswordVisibility hook
TwoFactorInput LoginForm / Settings 2FA 设置 / Settings 关闭 2FA 独立组件 + useTotpInput hook
EmailInput LoginForm / RegisterForm / 邀请成员 独立组件
OAuthButtons LoginForm / RegisterForm 独立组件 + provider 配置数组
BrandPanel LoginForm / RegisterForm / 自定义 SSO 页 独立组件 + brand props
AuthErrorBoundary 所有认证组件 复用 shared/components/widget-boundary.tsx
useFormState 所有表单 复用 shared/hooks/use-action-mutation.ts

6.5 错误与边界处理

6.5.1 Error Boundary 包裹

<AuthErrorBoundary fallback={<AuthErrorFallback />}>
  <LoginForm />
</AuthErrorBoundary>

6.5.2 边界状态处理

  • 空数据LoginForm 无 callbackUrl 时默认 /dashboard
  • 无权限proxy.ts 重定向时 URL 携带 ?reason=forbidden,目标页通过 useSearchParams 读取并 toast 提示
  • 网络异常AuthService 抛 NetworkErrorLoginForm 区分显示
  • 账户锁定:返回 AccountLockedError + lockedUntilLoginForm 显示倒计时
  • 速率限制:返回 RateLimitedError + retryAfterMsLoginForm 显示剩余时间

6.6 可测试性

6.6.1 纯函数导出

// src/modules/auth/services/password-service.ts
export function validatePasswordStrength(password: string): PasswordStrengthResult { ... }
export async function hashPassword(password: string): Promise<string> { ... }
export async function verifyPassword(password: string, hash: string): Promise<boolean> { ... }
export async function checkBreachedPassword(password: string): Promise<boolean> { ... }

6.6.2 单元测试

src/modules/auth/
├─ services/
│  ├─ password-service.test.ts
│  ├─ rate-limit-service.test.ts
│  └─ two-factor-service.test.ts
├─ data-access.test.ts
├─ schema.test.ts
└─ components/
   ├─ login-form.test.tsx
   └─ register-form.test.tsx

6.6.3 e2e 测试修正

// tests/e2e/smoke-auth.spec.ts重构后
test("login page renders required controls", async ({ page }) => {
  await page.goto("/login")
  await expect(page.getByTestId("login-form")).toBeVisible()
  await expect(page.getByLabel(/auth.login.email/i)).toBeVisible()
  await expect(page.getByLabel(/auth.login.password/i)).toBeVisible()
  await expect(page.getByRole("button", { name: /auth.login.signIn/i })).toBeVisible()
})

6.7 可扩展性:配置驱动

6.7.1 路由权限配置

// src/shared/lib/route-permissions.ts
export const ROUTE_PERMISSION_CONFIG = {
  routePrefix: {
    "/admin": Permissions.SCHOOL_MANAGE,
    "/teacher": Permissions.EXAM_CREATE,
    "/student": Permissions.HOMEWORK_SUBMIT,
    "/parent": Permissions.DASHBOARD_PARENT_READ,
    "/management": Permissions.GRADE_MANAGE,
  },
  dashboard: {
    "/admin/dashboard": Permissions.DASHBOARD_ADMIN_READ,
    "/teacher/dashboard": Permissions.DASHBOARD_TEACHER_READ,
    "/student/dashboard": Permissions.DASHBOARD_STUDENT_READ,
    "/parent/dashboard": Permissions.DASHBOARD_PARENT_READ,
  },
  specific: {
    "/admin/ai-settings": Permissions.AI_CHAT,
    "/admin/roles": Permissions.ROLE_READ,
    "/admin/permissions": Permissions.PERMISSION_READ,
    "/admin/announcements": Permissions.ANNOUNCEMENT_MANAGE,
    "/admin/audit-logs": Permissions.AUDIT_LOG_READ,
    // ... 可由 admin 端配置编辑
  },
  api: {
    "/api/ai/chat": Permissions.AI_CHAT,
  },
} as const

6.7.2 onboarding 步骤配置

// src/modules/onboarding/config/onboarding-steps.ts
export const ONBOARDING_STEPS: OnboardingStepConfig[] = [
  { key: "roleConfirm", component: "StepRoleConfirm", required: true },
  { key: "basicInfo", component: "StepBasicInfo", required: true },
  { key: "roleInfo", component: "StepRoleInfo", required: false, skip: { roles: ["admin"] } },
  { key: "emailVerification", component: "StepEmailVerification", required: false, enabled: false }, // 未来扩展
  { key: "complete", component: "StepComplete", required: true },
]

6.8 企业级补充

6.8.1 a11y

  • 所有输入框 aria-label / aria-describedby 关联错误
  • 错误 <p role="alert"> 动态提示
  • 键盘导航Tab 序、可见焦点环、Enter 提交
  • 显示密码按钮 Target Size ≥24×24
  • Caps Lock 检测 + aria-live="polite" 提示
  • 登录按钮加载态 aria-busy="true"

6.8.2 性能

  • LoginForm/RegisterForm 使用 "use client",页面其余部分 Server Component
  • AuthLayout 品牌 logo 使用 next/image 优先加载
  • onboarding-stepper 拆分后按步骤 lazy load
  • proxy.ts 路由权限判断保持在 middleware 层(不进入应用代码)

6.8.3 安全性

  • 注册接口:速率限制 + Zod + CAPTCHA + 邮箱验证 + 审计日志
  • 登录接口rate-limit + 账户锁定 + breached password 检测
  • 2FATOTP + 备用码 + WebAuthnP2
  • JWT 体积优化P1-7
  • sessions 表清理或切换策略P1-9

6.8.4 监控

  • 接入 trackEvent 埋点P1-11
  • admin/monitoring 页面展示核心指标P2-7
  • Sentry 接入异常告警P2-7

七、实施计划

7.1 第一阶段P0立即实施

  1. 新建 modules/auth/data-access.tsactions.ts

    • 迁移 register/page.tsx 内联 registerActionmodules/auth/actions.ts
    • 新增 createUsergetUserByEmailassignRoleToUserdata-access.ts
    • 删除页面内重复的 calcAge / normalizeBcryptHash,复用 shared/lib
    • 添加 RegisterSchema Zod 校验
    • 添加注册速率限制(独立 key register:{ip}
    • 添加 logLoginEvent({ action: "signup" }) 审计日志
    • 删除"自动创建 student 角色"逻辑
    • 错误消息不暴露 SQL 细节
  2. 抽取 modules/auth/services/two-factor-service.ts

    • modules/settings/actions-security.ts 提取 verifyTwoFactorForLogin / preflightTwoFactorAction
    • 通过 modules/settings/data-access-two-factor.ts 访问 DB
    • auth.ts 改为 await import("@/modules/auth/services/two-factor-service")
  3. 修复 onboarding 页面认证入口

    • app/(onboarding)/onboarding/page.tsx 改用 getAuthContext() 替代 auth()
  4. 删除废弃 OnboardingGate

    • 删除 src/shared/components/onboarding-gate.tsx
  5. 修复 auth.ts 中的 as 断言

    • 添加 isAuthUser 类型守卫
    • 删除 as typeof token.permissions 等断言
  6. 抽取 shared/lib/route-resolver.ts

    • 合并 proxy.ts#resolveDefaultPathonboarding/data-access.ts#resolveDefaultPathByRoles
    • 改用权限点优先 + 角色兜底
  7. 修复 e2e 测试

    • 重写 tests/e2e/smoke-auth.spec.ts 使用 i18n key 定位

7.2 第二阶段P1本周内

  1. 接入 i18n

    • LoginForm / RegisterForm / AuthLayout / error.tsx / not-found.tsx 接入 useTranslations
    • onboarding actions.ts / data-access.ts 接入 getTranslations
    • 扩展 auth.json 翻译键
  2. 拆分 onboarding-stepper

    • 拆为 4 个步骤组件 + useOnboardingForm hook
  3. 拆分 auth.ts authorize

    • 拆为 5 个纯函数 + service
  4. 添加 LoginForm 错误反馈细化

    • 区分 error code
    • ARIA 关联
    • 显示密码按钮
    • Caps Lock 检测
  5. 添加 (auth) 路由组 loading.tsx + Error Boundary

  6. rate-limit 抽象接口

    • 支持 memory / redis 双实现
  7. bindParentToChild 速率限制

  8. 接入 trackEvent

    • 新增 auth 事件类型
    • auth.ts events 调用 trackEvent

7.3 第三阶段P2中长期

  1. users 表拆分
  2. SSO/SAML 接入
  3. 邀请码预分配机制
  4. Breached Password 检测
  5. 登录审计日志保留期清理任务
  6. AuthLayout 品牌配置
  7. 监控仪表盘
  8. WebAuthn 支持

7.4 架构图同步

每完成一个 P0/P1 项,需同步更新 docs/architecture/004_architecture_impact_map.md005_architecture_data.json

  • 新增/删除的导出函数
  • 新增/删除的文件
  • 修改的依赖关系
  • 修改的权限点

八、验证清单

完成所有 P0/P1 项后,需通过以下验证:

  • npm run lint 零错误
  • npx tsc --noEmit 零错误
  • npm run test(新增的单元测试)全部通过
  • npm run e2e(修正后的 smoke-auth通过
  • 手动验证:注册 → 登录 → onboarding → 角色切换 → 登出 全流程
  • 手动验证账户锁定、速率限制、2FA 启用/关闭
  • 手动验证:中英文 locale 切换
  • 手动验证a11y 键盘导航、屏幕阅读器
  • 架构图 004/005 与代码一致

九、风险与回滚

风险 影响 缓解措施
registerAction 迁移可能改变错误消息格式 前端 toast 显示异常 保持 ActionState 结构不变e2e 覆盖
auth.ts 2FA 抽取可能破坏登录链路 用户无法登录 保留旧 import 路径作为 fallback分阶段灰度
onboarding 改用 getAuthContext 可能改变未登录行为 未登录用户访问 /onboarding 行为变化 添加 e2e 覆盖未登录场景
rate-limit 改为 Redis 可能引入网络延迟 登录响应变慢 内存实现作为默认Redis 为可选
JWT 体积优化可能改变权限刷新时机 权限撤销延迟变化 评估方案 A vs B 后再实施