66 KiB
认证模块审计报告
审计日期:2026-06-25 审计范围:
src/auth.ts、src/proxy.ts、src/next-auth.d.ts、src/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}.tsx、src/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}.ts、src/shared/hooks/use-permission.ts、src/shared/types/permissions.ts审计依据:e:\Desktop\CICD\.trae\rules\project_rules.md、docs/architecture/004_architecture_impact_map.md§2.24/§3/§4/§5、docs/architecture/005_architecture_data.jsonauth/usersAndAuth前序报告:docs/architecture/audit/permissions-audit-report.md、docs/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 → resolveDataScope(rbac resolver)→ data-access
1.3 架构图完整性
docs/architecture/004_architecture_impact_map.md §2.24(auth 认证 UI 模块)与 §3(根目录 auth.ts/proxy.ts)记录的导出函数、依赖关系与实际代码大体一致,但存在以下遗漏:
- 未记录
modules/onboarding:架构图 §2.24 仅描述modules/auth,但 onboarding 与 auth 强耦合(首次登录引导),未在 auth 章节中关联说明,仅在 §5usersAndAuth域中提及。 - 未记录
auth.ts对modules/settings的反向依赖:auth.ts#L106await import("@/modules/settings/actions-security")调用verifyTwoFactorForLogin,但架构图 §3 描述 auth.ts 仅依赖shared/lib/*,遗漏了跨层依赖。 - 未记录
proxy.ts的resolveDefaultPath函数:导出函数仅记录proxy主入口,但resolveDefaultPath被onboarding/data-access.ts重复实现(resolveDefaultPathByRoles),属于逻辑重复未在依赖矩阵中标记。 - 未记录
onboarding-gate.tsx:架构图shared/components清单中未列出此组件,但文件仍存在。 - 未记录
use-permission.ts与auth-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。同时文件内重复实现了calcAge、normalizeBcryptHash两个纯函数(前者与register-form.tsx#L24-35重复,后者与shared/lib/bcrypt-utils.ts#L15-19重复)。 - 违反规则:
- 项目规则"
app/只能调用modules/的 Server Actions 和 data-access,不直接访问数据库" - 项目规则"模块标准结构要求
data-access.ts、actions.ts分层" - 项目规则"Reusable UI components must be extracted to avoid JSX duplication"(此处是逻辑重复)
- 项目规则"
- 后果:注册逻辑无法被单测、无法被 mock;DB schema 变更需改页面文件;后续添加邮箱验证、审批流、邀请码等扩展需重写;与
modules/users/data-access.ts的getUserProfile/updateUserProfileById等同样操作users表的函数形成两套并行的写入口,职责割裂。
问题 2:src/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)会破坏 auth;
verifyTwoFactorForLogin被auth.ts调用导致其内部依赖的getTwoFactorEnabled/getTotpSecret/getBackupCodesHashed等函数在登录时被加载,bundle 体积无谓增加。
问题 3:onboarding 页面绕过统一认证入口
- 位置:onboarding/page.tsx
import { auth } from "@/auth"、const session = await auth() - 问题:项目硬约束明确要求"Authentication must use
getAuthContext()andgetCurrentStudentUser(); avoid mixing with other auth methods",但 onboarding 页面绕过shared/lib/auth-guard.getAuthContext(),直接调用auth()获取 session。同时仅靠if (!userId) redirect("/login")手动处理未登录态,不享受PermissionDeniedError统一异常与 DataScope 解析。 - 违反规则:
- 项目硬约束"Authentication must use
getAuthContext()andgetCurrentStudentUser()" - 项目 memory"Authentication must use
getAuthContext()andgetCurrentStudentUser(); avoid mixing with other auth methods"
- 项目硬约束"Authentication must use
- 后果:未登录异常未走
route-error.tsx统一处理;JWT session 与 DB 中users.onboardedAt不一致时只能靠手动redirect;与同项目中所有其他 dashboard 页面使用requireAuth()的模式不一致,认知成本增加。
问题 4:废弃的 OnboardingGate 仍残留在代码库
- 位置:onboarding-gate.tsx(313 行)
- 问题:架构图明确记录"P2-1 已修复:用 middleware 强制重定向替代客户端 Dialog",但
OnboardingGate文件仍存在且未被任何代码 import(grep 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)
问题 5:onboarding-stepper.tsx 451 行已接近 500 行上限
- 位置:onboarding-stepper.tsx
- 问题:单组件混合 4 步骤 UI 渲染、状态管理、子女行动态增删、URL 参数同步、表单提交、跳过逻辑、权限校验。文件中
step === 0/1/2/3分支硬编码,新增"邮箱验证"、"隐私同意"等步骤需改大组件。 - 违反规则:项目规则"React 组件 ≤ 500 行(复杂表单可放宽至 800 行)"虽未超出,但已接近上限,且组件未拆分。
- 后果:单测困难;步骤间复用无法实现;新增步骤需大改。
问题 6:auth.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
- 问题:
- 无速率限制:auth.ts authorize 有
rateLimit(RATE_LIMIT_RULES.LOGIN),但 registerAction 完全无限制 - 无 Zod 校验:仅
if (!email)/if (password.length < 6)字符串判断,未使用项目标准的OnboardingSchema模式 - 无邮箱验证流程:注册即激活,无验证邮件、无管理员审批
- 无 CAPTCHA:机器人可批量注册
- 无审计日志:注册成功未调用
logLoginEvent({ action: "signup", status: "success" })(schema 中loginLogActionEnum已包含 "signup",但实际未使用) - 角色查找静默失败并自动创建:第 92-94 行
if (!roleRow) { await db.insert(roles).values({ name: "student" }) }——任何访问 /register 的匿名用户都可触发roles表插入,且无isSystem: true、无isEnabled: true,污染 RBAC 配置 - 密码哈希 cost=10:低于推荐 cost=12;未做 breached password 检测
- 错误信息泄露数据库细节:第 158-162 行直接返回
创建账户失败:${msg}(含 sqlMessage)
- 无速率限制:auth.ts authorize 有
- 违反规则:
- 项目规则"Server Action 必须使用
requirePermission()进行权限校验"(注册虽无登录态,但应有等效防护) - 项目规则"输入使用 Zod 验证,验证失败返回结构化错误"
- 项目安全规范"JWT/session ID 存储在 httpOnly + Secure + SameSite=Strict 的 Cookie 中"
- 项目规则"禁止
dangerouslySetInnerHTML"——此处虽未用,但错误消息直接暴露 SQL 错误违反同源安全原则
- 项目规则"Server Action 必须使用
- 后果:批量注册攻击、roles 表污染、SQL 错误信息泄露、注册行为无法审计与监控。
问题 8:proxy.ts 的 resolveDefaultPath 硬编码角色名
- 位置: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 模块动态角色支持不一致;两份实现可能漂移。
问题 9:onboarding 完成时角色信息从 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)
问题 10:login-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的模式不一致。
问题 11:register-form.tsx 硬编码中文,未使用 i18n
- 位置:register-form.tsx
- 问题:硬编码 "创建账户"、"填写以下信息以创建您的账户"、"姓名"、"邮箱"、"密码"、"出生日期"、"当前年龄:{age} 岁"、"检测到您是未成年人,请填写监护人信息"、"监护人姓名"、"监护人电话"、"监护人与您的关系"、"父亲"、"母亲"、"其他法定监护人"、"我已阅读并同意"、"《隐私政策》"、"《用户协议》"、"我确认已获得监护人同意使用本服务"、"创建账户"、"已有账户?"、"立即登录"。
- 违反规则:同上。
- 后果:英文用户无法理解;翻译键已存在但未使用;toast 错误消息也硬编码。
问题 12:onboarding actions.ts 与 data-access.ts 错误消息硬编码中文
- 位置:
- 问题:硬编码 "输入校验失败"、"班级码绑定失败"、"Onboarding 提交失败"、"未找到该邮箱对应的学生"、"学生未设置出生日期,请联系管理员"、"子女生日不匹配"、"子女手机号后 4 位不匹配"。
onboarding.json#toast已有submitFailed、inputInvalid等键,但 Server Action 未通过getTranslations读取。 - 违反规则:项目规则"所有用户可见文本必须适配 i18n"。
- 后果:英文 locale 下 toast 显示中文;与项目其他模块 Server Action 已迁移至 i18n 的模式不一致。
问题 13:(auth) 路由组的 error.tsx 与 not-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(混合中英文)语言不一致。
问题 14:privacy/page.tsx 与 terms/page.tsx 静态中文
- 位置:
- 问题:整页隐私政策与用户协议硬编码中文,未提取 i18n 键。
- 违反规则:项目规则"所有用户可见文本必须适配 i18n"。
- 后果:英文 locale 下展示中文;维护政策文本需改代码而非改 JSON。
问题 15:AuthLayout 左侧品牌 testimonial 硬编码英文且与 K12 场景不符
- 位置:auth-layout.tsx#L19-L22
- 问题:
硬编码英文 testimonial 与虚构人物 "Sofia Davis",与 K12 教育系统定位不符。
<blockquote className="space-y-2"> <p className="text-lg"> “This platform has completely transformed how we deliver education to our students. The attention to detail and performance is unmatched.” </p> <footer className="text-sm">Sofia Davis</footer> </blockquote> - 违反规则:项目规则"所有用户可见文本必须适配 i18n"。
- 后果:i18n 失效;学校白标部署无法配置 testimonial;与产品定位不符。
2.5 错误处理与边界(P1)
问题 16:LoginForm 未区分账户锁定、速率限制、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 缺失与密码错误提示混淆。
问题 17:LoginForm 的 preflightTwoFactorAction 失败时静默降级
- 位置: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) 路由组模式不一致。
问题 19:LoginForm 与 RegisterForm 无 React Error Boundary 包裹
- 位置:login/page.tsx、register/page.tsx
- 问题:仅
<Suspense fallback={null}>包裹 LoginForm,无 Error Boundary;如果组件抛错会冒泡到error.tsx全页错误,用户丢失已输入数据。 - 违反规则:项目规则"每个独立的数据区块必须用 React Error Boundary 包裹"。
- 后果:单个组件错误导致整页不可用。
问题 20:proxy.ts 重定向时未携带原始路径到 onboarding 完成后回跳
- 位置:proxy.ts#L84-L90
- 问题:未登录用户访问
/teacher/classes时被重定向到/login?callbackUrl=...,但登录成功后若 onboarding 未完成又被重定向到/onboarding,onboarding 完成后又跳转到默认路径/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)
问题 21:auth.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 ?? ""
- auth.ts#L146
- 问题:使用
as断言而非类型守卫。next-auth.d.ts已声明 Session/JWT 类型,但 jwt callback 中user参数是any(NextAuth 类型缺陷),需用类型守卫。 - 违反规则:项目规则"禁止
as断言(除类型收窄外)"。 - 后果:类型与运行时不一致;重构易引入 bug;jwt callback 中 token.permissions 类型与 Permission[] 不兼容。
问题 22:proxy.ts 中 (token.roles as string[]) 断言
- 位置:proxy.ts#L93,99
- 问题:直接
as string[],未做类型守卫;旧 token 数据格式异常时(如 token 为 v1 格式无 roles 字段)会 runtime error。 - 违反规则:项目规则"禁止
as断言"。 - 后果:旧 token 用户访问时崩溃。
问题 23:register-form.tsx ActionState 未指定泛型
- 位置:register-form.tsx#L20,38
- 问题:
import type { ActionState } from "@/shared/types/action-state"与registerAction: (formData: FormData) => Promise<ActionState>未指定泛型参数T,data字段类型为unknown。 - 违反规则:项目规则"函数返回值必须显式标注"。
- 后果:success 分支访问
res.data时无类型提示。
2.7 单测不可行(P0)
问题 24:auth 模块几乎无单元测试,e2e 测试与 UI 不一致
- 位置:tests/e2e/smoke-auth.spec.ts
- 问题:
rate-limit.ts、password-policy.ts、role-utils.ts、bcrypt-utils.ts、http-utils.ts、password-security-service.ts、session.ts、auth-guard.ts、permissions.ts(resolvePermissions)均无对应.test.tsauth.ts的 authorize/jwt/session 回调无测试proxy.ts的路由权限判断无测试- e2e 测试期望
getByLabel("Full Name")与button { name: "Create Account" }(英文),但register-form.tsx实际是中文"姓名"和"创建账户"——e2e 必然失败
- 违反规则:项目规则"数据获取、计算、格式化等纯逻辑全部放入纯函数或 hooks,与 UI 分离;导出清晰的接口类型以便 mock"。
- 后果:重构无回归保障;rate-limit 改为 Redis 时无对照基线;e2e 在 CI 失败或被 skip。
2.8 组件不可复用(P1)
问题 25:LoginForm / RegisterForm 无法被复用与测试
- 位置:login-form.tsx、register-form.tsx
- 问题:
- LoginForm 硬编码
signIn("credentials"),未抽象AuthService接口,与next-auth/react紧耦合 - RegisterForm 接受
registerActionprops 但未定义数据契约接口(Promise<ActionState>无泛型) - 两个组件均直接 import
@/modules/settings/actions-security(login-form)和通过 props 注入(register-form),方式不统一 - GitHub 登录按钮是死链无 onClick
- LoginForm 硬编码
- 违反规则:项目规则"完全解耦:通过定义 TypeScript 接口抽象数据依赖,使用 React Context 注入数据服务"。
- 后果:换 SSO 提供商需重写组件;无法在测试中 mock;多角色场景下无法复用。
问题 26:AuthLayout 静态硬编码品牌信息
- 位置:auth-layout.tsx#L12-L22
- 问题:
GraduationCap图标、"Next_Edu" 品牌名、"Sofia Davis" testimonial 全部硬编码,无 props 注入。 - 违反规则:项目规则"最大化复用:识别四个角色共用的 UI 块和业务逻辑块,抽象为泛型组件和 hooks"。
- 后果:学校白标部署无法配置 logo/标语;无法在 admin 设置页预览登录页样式。
2.9 安全性(P0/P1)
问题 27:rate-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 水平扩容后暴力破解防护失效。
问题 28:JWT 中存储 permissions 数组导致 token 体积过大
- 位置:auth.ts#L153,184
token.permissions = await resolvePermissions(allRoles) - 问题:67 个权限点字符串数组写入 JWT(admin 角色约 67 个 × 平均 20 字符 = ~1.3KB),JWT cookie 体积可能超过 4KB 限制;权限变更需用户重新登录或 JWT refresh 才能生效。
- 违反规则:项目规则"JWT/session ID 存储在 httpOnly + Secure + SameSite=Strict 的 Cookie 中"——cookie 体积过大可能被浏览器截断。
- 后果:cookie 体积超限导致 session 丢失;权限撤销有延迟;admin 角色拥有最多权限,最易触发。
问题 29:onboarding/data-access.ts bindParentToChild 未做独立速率限制
- 位置:onboarding/data-access.ts#bindParentToChild
- 问题:家长绑定子女的三因子验证(生日 365 × 手机后4 10000 = 3.65M)虽然组合空间大,但单次 onboarding Action 内可循环调用 10 次(
children.length ≤ 10),无独立速率限制;被撤销子女关系的家长可反复尝试枚举绑定。 - 违反规则:项目安全规范。
- 后果:枚举攻击窗口;audit_logs 仅记录成功,未记录失败次数。
问题 30:sessions 表存在但 JWT 策略下未使用,远程登出功能形同虚设
- 位置:
- schema.ts#L84
sessions表定义 - auth.ts#L25
session: { strategy: "jwt" } - actions-security.ts#revokeAllOtherSessionsAction#L362-L403
- schema.ts#L84
- 问题:NextAuth 配置 JWT 策略,但 schema 中
sessions表仍存在(无数据);revokeAllOtherSessionsAction删 sessions 表但实际无活跃记录,注释承认 "JWT-based, no active DB sessions"。 - 违反规则:项目规则"避免 backwards-compatibility 残留"。
- 后果:用户误以为可远程登出其他设备;sessions 表 schema 与运行时不一致,增加困惑。
问题 31:proxy.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)
问题 32:LoginForm 无 ARIA 错误关联、无"显示密码"按钮、无 Caps Lock 检测
- 位置:login-form.tsx#L89,111-117,156-158
- 问题:
- 错误
<p className="text-sm text-red-600">{error}</p>未使用role="alert"、未aria-describedby关联到输入框 - 密码输入框无显示/隐藏切换
- 无 Caps Lock 检测提示
- GitHub 登录按钮无 onClick 是死链
- 错误
- 违反规则:项目规则"可访问性(a11y):语义化标签、ARIA 属性、键盘导航"。
- 后果:屏幕阅读器用户无法感知错误;违反 WCAG 2.2 Target Size 与 Focus Not Obscured。
问题 33:(auth) 路由组无 loading.tsx、无骨架屏
- 位置:
src/app/(auth)/(缺文件) - 问题:路由切换无骨架屏;login-form 内部用
isLoadingstate 控制按钮禁用,但无整体加载态。 - 违反规则:项目规则"异步数据使用 React Suspense + 骨架屏"。
- 后果:网络慢时用户体验差。
2.11 监控埋点(P1)
问题 34:auth 模块未定义任何 trackEvent 事件
- 位置:track-event.ts#EventName
- 问题:
EventName类型枚举中无auth.signin、auth.signout、auth.signup、auth.2fa_enabled、auth.password_reset、auth.account_locked、auth.rate_limited等。 - 违反规则:项目规则"监控:方案中预留关键操作埋点接口"。
- 后果:登录成功率、2FA 启用率、账户锁定触发率等核心指标无法统计;无法接 Sentry/PostHog。
问题 35:auth.ts events.signIn/signOut 仅写 login_logs,未触发业务埋点
- 位置:auth.ts#L205-L232
- 问题:登录成功仅
logLoginEvent,未void trackEvent({ name: "auth.signin", ... })。 - 违反规则:项目规则"监控:方案中预留关键操作埋点接口"。
- 后果:监控仪表盘缺失;无法告警异常登录地理/设备。
2.12 可扩展性(P1)
问题 36:proxy.ts 路由权限配置硬编码,无配置驱动
- 位置:proxy.ts#L13-L43
- 问题:
ROUTE_PERMISSIONS、DASHBOARD_ROUTE_PERMISSIONS、SPECIFIC_ROUTE_PERMISSIONS、API_PERMISSIONS4 个常量写死在文件中,新增角色或权限点需改代码。 - 违反规则:项目规则"可扩展性:采用配置驱动设计"。
- 后果:与 RBAC 模块的
DATA_SCOPE_RULES配置驱动不一致;新增路由时易遗漏权限配置。
问题 37:onboarding 流程不支持配置驱动
- 位置:onboarding-stepper.tsx
- 问题:步骤硬编码 4 步(roleConfirm/basicInfo/roleInfo/complete),新增"邮箱验证"、"隐私同意"、"家长扫码绑定子女"等步骤需改组件代码。
- 违反规则:项目规则"可扩展性:采用配置驱动设计,例如通过角色配置决定该模块渲染哪些 Widget/子模块"。
- 后果:扩展性差;与项目其他模块(如 dashboard widget-configs.ts)的配置驱动模式不一致。
2.13 数据库与 Schema 一致性(P2)
问题 38:users 表混合了认证、用户资料、未成年人保护等多类字段
- 位置:schema.ts#L27-L57
- 问题:
users表 31 个字段混合:认证(password, emailVerified, image)、用户资料(name, phone, address, gender, age)、未成年人保护(birthDate, guardianName, guardianPhone, guardianRelation, consentAcceptedAt)、组织(gradeId, departmentId)、onboarding(onboardedAt)。 - 违反规则:项目规则"模块标准结构要求 schema 按职责拆分"。
- 后果:schema 文件 1111 行(架构图 P0-1 已记录);认证模块修改 password 字段时影响用户资料模块;难以按表分库分表。
三、行业差距对比
3.1 K12 认证主流模式对标
| 维度 | 行业最佳实践 | 本项目现状 | 差距 |
|---|---|---|---|
| 账号创建 | 管理员预分配 + 开放注册关闭(PowerSchool、Veracross、Clever) | 开放注册,任何人可注册 student 账号 | 重大差距:未实现邀请码/批量导入预分配;任何人可污染 student 角色池 |
| SSO 单点登录 | Clever SSO(2750 万师生)/ 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 throttling,5-10 次失败后临时锁定 15-30 分钟 | 5 次失败后锁定 30 分钟(PASSWORD_RULES) | 达标 |
| 速率限制 | 按 IP + 账号双维度,登录端点 ≥10 req/min + 指数退避 | 5 attempts / 15 min(IP+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-label、aria-invalid、aria-describedby、role="alert" |
LoginForm 错误 p 标签无 role="alert" | 差距 |
| 键盘导航 | 完整 Tab 序、可见焦点环(对比度 ≥3:1) | 基本支持,但未显式验证 | 部分达标 |
| i18n | PowerSchool 支持 EN/ES/ZH,URL 持久化 | 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.ts 与 modules/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(),未登录抛 PermissionDeniedError 由 route-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 细节;⑥ 接入 CAPTCHA(hCaptcha 或 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.ts 与 onboarding/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 个纯函数 + service,authorize 仅编排 |
| P1-4 | LoginForm 错误反馈细化(问题 16/17/32) | 区分 error code(CredentialsSignin/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) | 方案 A:JWT 仅存 userId + roles,permissions 每次请求通过 getAuthContext() 从 DB/缓存查询(牺牲少量性能换 token 体积);方案 B:JWT 存 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_limited;auth.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_onboarding(onboardedAt);通过 view 或 data-access 层聚合 |
| P2-2 | 引入 SSO/SAML/OIDC | 接入 NextAuth 的 GoogleProvider/MicrosoftEntraIDProvider/SAMLProvider;admin 端配置 IdP 元数据 |
| P2-3 | 引入邀请码预分配机制 | 新增 invitation_codes 表(code/email/role/classId/createdAt/expiresAt/usedBy);admin 批量生成邀请码替代开放注册 |
| P2-4 | Breached Password 检测 | 接入 haveibeenpwned API(k-anonymity 范围查询);注册/改密时校验 |
| P2-5 | 登录审计日志保留期 | 复用 audit 模块的 retention.ts,为 login_logs 配置 1 年保留期;添加定时任务清理 |
| P2-6 | AuthLayout 品牌配置 | admin 端新增"学校品牌"配置(logo/配色/标语/testimonial);AuthLayout 通过 props 注入 |
| P2-7 | 监控仪表盘 | 新增 admin/monitoring 页面展示登录成功率/2FA 启用率/锁定触发率;接入 Sentry/PostHog |
| P2-8 | WebAuthn 支持 | 接入 @simplewebauthn/server;2FA 设置页新增 WebAuthn 选项 |
五、架构图同步说明
本次审计发现架构图(004_architecture_impact_map.md 与 005_architecture_data.json)需补充/修改以下节点:
5.1 需新增节点
modules/onboarding作为独立模块章节(§2.X),导出getOnboardingStatusAction/completeOnboardingAction/OnboardingSchema,依赖shared/*+modules/classes/data-access+modules/rbac/lib/data-scope-resolver(间接)。shared/components/onboarding-gate.tsx加入 shared/components 清单,标记为"已废弃,待删除"。shared/lib/route-resolver.ts(重构后新增)作为 proxy.ts 与 onboarding/data-access.ts 共用的纯函数。
5.2 需修改节点
- §3 根目录 auth.ts:
- 依赖列表新增
modules/settings/actions-security(当前遗漏) - 标注"重构后将改为依赖
modules/auth/services/two-factor-service"
- 依赖列表新增
- §2.24 modules/auth:
- 文件清单新增
data-access.ts、actions.ts、schema.ts、types.ts、services/two-factor-service.ts(重构后) - 删除"纯 UI 模块,无 data-access/actions/types"描述
- 文件清单新增
- §3 proxy.ts:
- 导出函数列表新增
resolveDefaultPath(当前遗漏) - 依赖列表新增
shared/lib/route-resolver(重构后)
- 导出函数列表新增
shared/lib清单:- 新增
route-permissions.ts、route-resolver.ts(重构后)
- 新增
5.3 需删除节点
shared/components/onboarding-gate.tsx标记为"待删除(P0-4)",重构后从清单移除。app/api/onboarding/status/route.ts与app/api/onboarding/complete/route.ts标记为"已废弃(410 Gone)",重构后可删除。
六、重构方案设计
6.1 完全解耦:AuthService 接口抽象与依赖注入
6.1.1 模块边界重新划分
src/modules/auth/
├─ actions.ts # Server Actions(registerAction / loginRateLimitedAction)
├─ data-access.ts # data-access(createUser / getUserByEmail / 等)
├─ schema.ts # Zod(RegisterSchema / 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 抛
NetworkError,LoginForm 区分显示 - 账户锁定:返回
AccountLockedError+lockedUntil,LoginForm 显示倒计时 - 速率限制:返回
RateLimitedError+retryAfterMs,LoginForm 显示剩余时间
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 检测
- 2FA:TOTP + 备用码 + WebAuthn(P2)
- JWT 体积优化(P1-7)
- sessions 表清理或切换策略(P1-9)
6.8.4 监控
- 接入
trackEvent埋点(P1-11) - admin/monitoring 页面展示核心指标(P2-7)
- Sentry 接入异常告警(P2-7)
七、实施计划
7.1 第一阶段(P0,立即实施)
-
新建
modules/auth/data-access.ts与actions.ts- 迁移
register/page.tsx内联registerAction到modules/auth/actions.ts - 新增
createUser、getUserByEmail、assignRoleToUser到data-access.ts - 删除页面内重复的
calcAge/normalizeBcryptHash,复用shared/lib - 添加
RegisterSchemaZod 校验 - 添加注册速率限制(独立 key
register:{ip}) - 添加
logLoginEvent({ action: "signup" })审计日志 - 删除"自动创建 student 角色"逻辑
- 错误消息不暴露 SQL 细节
- 迁移
-
抽取
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")
- 从
-
修复 onboarding 页面认证入口
app/(onboarding)/onboarding/page.tsx改用getAuthContext()替代auth()
-
删除废弃 OnboardingGate
- 删除
src/shared/components/onboarding-gate.tsx
- 删除
-
修复
auth.ts中的as断言- 添加
isAuthUser类型守卫 - 删除
as typeof token.permissions等断言
- 添加
-
抽取
shared/lib/route-resolver.ts- 合并
proxy.ts#resolveDefaultPath与onboarding/data-access.ts#resolveDefaultPathByRoles - 改用权限点优先 + 角色兜底
- 合并
-
修复 e2e 测试
- 重写
tests/e2e/smoke-auth.spec.ts使用 i18n key 定位
- 重写
7.2 第二阶段(P1,本周内)
-
接入 i18n
- LoginForm / RegisterForm / AuthLayout / error.tsx / not-found.tsx 接入
useTranslations - onboarding actions.ts / data-access.ts 接入
getTranslations - 扩展
auth.json翻译键
- LoginForm / RegisterForm / AuthLayout / error.tsx / not-found.tsx 接入
-
拆分 onboarding-stepper
- 拆为 4 个步骤组件 +
useOnboardingFormhook
- 拆为 4 个步骤组件 +
-
拆分 auth.ts authorize
- 拆为 5 个纯函数 + service
-
添加 LoginForm 错误反馈细化
- 区分 error code
- ARIA 关联
- 显示密码按钮
- Caps Lock 检测
-
添加 (auth) 路由组 loading.tsx + Error Boundary
-
rate-limit 抽象接口
- 支持 memory / redis 双实现
-
bindParentToChild 速率限制
-
接入 trackEvent
- 新增 auth 事件类型
- auth.ts events 调用 trackEvent
7.3 第三阶段(P2,中长期)
- users 表拆分
- SSO/SAML 接入
- 邀请码预分配机制
- Breached Password 检测
- 登录审计日志保留期清理任务
- AuthLayout 品牌配置
- 监控仪表盘
- WebAuthn 支持
7.4 架构图同步
每完成一个 P0/P1 项,需同步更新 docs/architecture/004_architecture_impact_map.md 与 005_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 后再实施 |