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

1095 lines
66 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 认证模块审计报告
> 审计日期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.json` `auth`/`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 → 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.ts` 对 `modules/settings` 的反向依赖**`auth.ts#L106` `await import("@/modules/settings/actions-security")` 调用 `verifyTwoFactorForLogin`,但架构图 §3 描述 auth.ts 仅依赖 `shared/lib/*`,遗漏了跨层依赖。
3. **未记录 `proxy.ts` 的 `resolveDefaultPath` 函数**:导出函数仅记录 `proxy` 主入口,但 `resolveDefaultPath``onboarding/data-access.ts` 重复实现(`resolveDefaultPathByRoles`),属于逻辑重复未在依赖矩阵中标记。
4. **未记录 `onboarding-gate.tsx`**:架构图 `shared/components` 清单中未列出此组件,但文件仍存在。
5. **未记录 `use-permission.ts` 与 `auth-session-provider.tsx` 在 auth 模块中的归属**:被随意划入 shared但语义上属于 auth 模块对外暴露的客户端 API。
---
## 二、现存问题与原因分析
### 2.1 三层架构违规P0
#### 问题 1注册页面 Server Action 内联直接访问数据库
- **位置**[register/page.tsx](file:///e:/Desktop/CICD/src/app/(auth)/register/page.tsx#L36-L164)
- **问题**`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"(此处是逻辑重复)
- **后果**:注册逻辑无法被单测、无法被 mockDB schema 变更需改页面文件;后续添加邮箱验证、审批流、邀请码等扩展需重写;与 `modules/users/data-access.ts``getUserProfile`/`updateUserProfileById` 等同样操作 `users` 表的函数形成两套并行的写入口,职责割裂。
#### 问题 2`src/auth.ts` 跨越 modules 边界依赖 settings 模块
- **位置**[auth.ts](file:///e:/Desktop/CICD/src/auth.ts#L106) `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 体积无谓增加。
#### 问题 3onboarding 页面绕过统一认证入口
- **位置**[onboarding/page.tsx](file:///e:/Desktop/CICD/src/app/(onboarding)/onboarding/page.tsx#L6-L19) `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.tsx](file:///e:/Desktop/CICD/src/shared/components/onboarding-gate.tsx)313 行)
- **问题**:架构图明确记录"P2-1 已修复:用 middleware 强制重定向替代客户端 Dialog",但 `OnboardingGate` 文件仍存在且未被任何代码 import`grep OnboardingGate` 仅返回定义行)。文件内含 `isAdmin`/`isTeacher`/`isStudent`/`isParent` 用权限点反推角色的硬编码逻辑:
```ts
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](file:///e:/Desktop/CICD/src/modules/onboarding/components/onboarding-stepper.tsx)
- **问题**:单组件混合 4 步骤 UI 渲染、状态管理、子女行动态增删、URL 参数同步、表单提交、跳过逻辑、权限校验。文件中 `step === 0/1/2/3` 分支硬编码,新增"邮箱验证"、"隐私同意"等步骤需改大组件。
- **违反规则**:项目规则"React 组件 ≤ 500 行(复杂表单可放宽至 800 行)"虽未超出,但已接近上限,且组件未拆分。
- **后果**:单测困难;步骤间复用无法实现;新增步骤需大改。
#### 问题 6`auth.ts` authorize 回调仍混合 7 类职责
- **位置**[auth.ts#L34-L141](file:///e:/Desktop/CICD/src/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](file:///e:/Desktop/CICD/src/app/(auth)/register/page.tsx#L36-L164)
- **问题**
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 错误信息泄露、注册行为无法审计与监控。
#### 问题 8`proxy.ts` 的 `resolveDefaultPath` 硬编码角色名
- **位置**[proxy.ts#L45-L52](file:///e:/Desktop/CICD/src/proxy.ts#L45-L52)
```ts
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](file:///e:/Desktop/CICD/src/modules/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](file:///e:/Desktop/CICD/src/modules/auth/components/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](file:///e:/Desktop/CICD/src/modules/auth/components/register-form.tsx)
- **问题**:硬编码 "创建账户"、"填写以下信息以创建您的账户"、"姓名"、"邮箱"、"密码"、"出生日期"、"当前年龄:{age} 岁"、"检测到您是未成年人,请填写监护人信息"、"监护人姓名"、"监护人电话"、"监护人与您的关系"、"父亲"、"母亲"、"其他法定监护人"、"我已阅读并同意"、"《隐私政策》"、"《用户协议》"、"我确认已获得监护人同意使用本服务"、"创建账户"、"已有账户?"、"立即登录"。
- **违反规则**:同上。
- **后果**英文用户无法理解翻译键已存在但未使用toast 错误消息也硬编码。
#### 问题 12onboarding `actions.ts` 与 `data-access.ts` 错误消息硬编码中文
- **位置**
- [onboarding/actions.ts#L94,130,147,159,225](file:///e:/Desktop/CICD/src/modules/onboarding/actions.ts#L94)
- [onboarding/data-access.ts#L90-103](file:///e:/Desktop/CICD/src/modules/onboarding/data-access.ts#L90-L103)
- **问题**:硬编码 "输入校验失败"、"班级码绑定失败"、"Onboarding 提交失败"、"未找到该邮箱对应的学生"、"学生未设置出生日期,请联系管理员"、"子女生日不匹配"、"子女手机号后 4 位不匹配"。`onboarding.json#toast` 已有 `submitFailed`、`inputInvalid` 等键,但 Server Action 未通过 `getTranslations` 读取。
- **违反规则**:项目规则"所有用户可见文本必须适配 i18n"。
- **后果**:英文 locale 下 toast 显示中文;与项目其他模块 Server Action 已迁移至 i18n 的模式不一致。
#### 问题 13`(auth)` 路由组的 `error.tsx` 与 `not-found.tsx` 静态英文
- **位置**
- [error.tsx](file:///e:/Desktop/CICD/src/app/(auth)/error.tsx)
- [not-found.tsx](file:///e:/Desktop/CICD/src/app/(auth)/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` 静态中文
- **位置**
- [privacy/page.tsx](file:///e:/Desktop/CICD/src/app/(auth)/privacy/page.tsx)
- [terms/page.tsx](file:///e:/Desktop/CICD/src/app/(auth)/terms/page.tsx)
- **问题**:整页隐私政策与用户协议硬编码中文,未提取 i18n 键。
- **违反规则**:项目规则"所有用户可见文本必须适配 i18n"。
- **后果**:英文 locale 下展示中文;维护政策文本需改代码而非改 JSON。
#### 问题 15`AuthLayout` 左侧品牌 testimonial 硬编码英文且与 K12 场景不符
- **位置**[auth-layout.tsx#L19-L22](file:///e:/Desktop/CICD/src/modules/auth/components/auth-layout.tsx#L19-L22)
- **问题**
```tsx
<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
#### 问题 16`LoginForm` 未区分账户锁定、速率限制、2FA 缺失等错误
- **位置**[login-form.tsx#L62-L69](file:///e:/Desktop/CICD/src/modules/auth/components/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](file:///e:/Desktop/CICD/src/modules/auth/components/login-form.tsx#L44-L46)
```ts
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](file:///e:/Desktop/CICD/src/app/(auth)/login/page.tsx)、[register/page.tsx](file:///e:/Desktop/CICD/src/app/(auth)/register/page.tsx)
- **问题**:仅 `<Suspense fallback={null}>` 包裹 LoginForm无 Error Boundary如果组件抛错会冒泡到 `error.tsx` 全页错误,用户丢失已输入数据。
- **违反规则**:项目规则"每个独立的数据区块必须用 React Error Boundary 包裹"。
- **后果**:单个组件错误导致整页不可用。
#### 问题 20`proxy.ts` 重定向时未携带原始路径到 onboarding 完成后回跳
- **位置**[proxy.ts#L84-L90](file:///e:/Desktop/CICD/src/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](file:///e:/Desktop/CICD/src/auth.ts#L146) `const u = user as { id: string; role?: string; roles?: string[]; name?: string }`
- [auth.ts#L196](file:///e:/Desktop/CICD/src/auth.ts#L196) `session.user.permissions = (token.permissions ?? []) as typeof token.permissions`
- [auth.ts#L217-L223](file:///e:/Desktop/CICD/src/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` 参数是 `any`NextAuth 类型缺陷),需用类型守卫。
- **违反规则**:项目规则"禁止 `as` 断言(除类型收窄外)"。
- **后果**:类型与运行时不一致;重构易引入 bugjwt callback 中 token.permissions 类型与 Permission[] 不兼容。
#### 问题 22`proxy.ts` 中 `(token.roles as string[])` 断言
- **位置**[proxy.ts#L93,99](file:///e:/Desktop/CICD/src/proxy.ts#L93-L99)
- **问题**:直接 `as string[]`,未做类型守卫;旧 token 数据格式异常时(如 token 为 v1 格式无 roles 字段)会 runtime error。
- **违反规则**:项目规则"禁止 `as` 断言"。
- **后果**:旧 token 用户访问时崩溃。
#### 问题 23`register-form.tsx` `ActionState` 未指定泛型
- **位置**[register-form.tsx#L20,38](file:///e:/Desktop/CICD/src/modules/auth/components/register-form.tsx#L20-L38)
- **问题**`import type { ActionState } from "@/shared/types/action-state"` 与 `registerAction: (formData: FormData) => Promise<ActionState>` 未指定泛型参数 `T``data` 字段类型为 `unknown`。
- **违反规则**:项目规则"函数返回值必须显式标注"。
- **后果**success 分支访问 `res.data` 时无类型提示。
### 2.7 单测不可行P0
#### 问题 24auth 模块几乎无单元测试e2e 测试与 UI 不一致
- **位置**[tests/e2e/smoke-auth.spec.ts](file:///e:/Desktop/CICD/tests/e2e/smoke-auth.spec.ts)
- **问题**
1. `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.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
#### 问题 25`LoginForm` / `RegisterForm` 无法被复用与测试
- **位置**[login-form.tsx](file:///e:/Desktop/CICD/src/modules/auth/components/login-form.tsx)、[register-form.tsx](file:///e:/Desktop/CICD/src/modules/auth/components/register-form.tsx)
- **问题**
- LoginForm 硬编码 `signIn("credentials")`,未抽象 `AuthService` 接口,与 `next-auth/react` 紧耦合
- RegisterForm 接受 `registerAction` props 但未定义数据契约接口(`Promise<ActionState>` 无泛型)
- 两个组件均直接 import `@/modules/settings/actions-security`login-form和通过 props 注入register-form方式不统一
- GitHub 登录按钮是死链无 onClick
- **违反规则**:项目规则"完全解耦:通过定义 TypeScript 接口抽象数据依赖,使用 React Context 注入数据服务"。
- **后果**:换 SSO 提供商需重写组件;无法在测试中 mock多角色场景下无法复用。
#### 问题 26`AuthLayout` 静态硬编码品牌信息
- **位置**[auth-layout.tsx#L12-L22](file:///e:/Desktop/CICD/src/modules/auth/components/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](file:///e:/Desktop/CICD/src/shared/lib/rate-limit.ts#L1-L9)
```ts
/**
* 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](file:///e:/Desktop/CICD/src/auth.ts#L153) `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 角色拥有最多权限,最易触发。
#### 问题 29`onboarding/data-access.ts` `bindParentToChild` 未做独立速率限制
- **位置**[onboarding/data-access.ts#bindParentToChild](file:///e:/Desktop/CICD/src/modules/onboarding/data-access.ts#L80-L127)
- **问题**:家长绑定子女的三因子验证(生日 365 × 手机后4 10000 = 3.65M)虽然组合空间大,但单次 onboarding Action 内可循环调用 10 次(`children.length ≤ 10`),无独立速率限制;被撤销子女关系的家长可反复尝试枚举绑定。
- **违反规则**:项目安全规范。
- **后果**枚举攻击窗口audit_logs 仅记录成功,未记录失败次数。
#### 问题 30`sessions` 表存在但 JWT 策略下未使用,远程登出功能形同虚设
- **位置**
- [schema.ts#L84](file:///e:/Desktop/CICD/src/shared/db/schema.ts#L84) `sessions` 表定义
- [auth.ts#L25](file:///e:/Desktop/CICD/src/auth.ts#L25) `session: { strategy: "jwt" }`
- [actions-security.ts#revokeAllOtherSessionsAction#L362-L403](file:///e:/Desktop/CICD/src/modules/settings/actions-security.ts#L362-L403)
- **问题**NextAuth 配置 JWT 策略,但 schema 中 `sessions` 表仍存在(无数据);`revokeAllOtherSessionsAction` 删 sessions 表但实际无活跃记录,注释承认 "JWT-based, no active DB sessions"。
- **违反规则**:项目规则"避免 backwards-compatibility 残留"。
- **后果**用户误以为可远程登出其他设备sessions 表 schema 与运行时不一致,增加困惑。
#### 问题 31`proxy.ts` 未对 `/admin/*` 子路径做细粒度权限校验
- **位置**[proxy.ts#L13-L19](file:///e:/Desktop/CICD/src/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](file:///e:/Desktop/CICD/src/modules/auth/components/login-form.tsx)
- **问题**
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](file:///e:/Desktop/CICD/src/shared/lib/track-event.ts)
- **问题**`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](file:///e:/Desktop/CICD/src/auth.ts#L205-L232)
- **问题**:登录成功仅 `logLoginEvent`,未 `void trackEvent({ name: "auth.signin", ... })`。
- **违反规则**:项目规则"监控:方案中预留关键操作埋点接口"。
- **后果**:监控仪表盘缺失;无法告警异常登录地理/设备。
### 2.12 可扩展性P1
#### 问题 36`proxy.ts` 路由权限配置硬编码,无配置驱动
- **位置**[proxy.ts#L13-L43](file:///e:/Desktop/CICD/src/proxy.ts#L13-L43)
- **问题**`ROUTE_PERMISSIONS`、`DASHBOARD_ROUTE_PERMISSIONS`、`SPECIFIC_ROUTE_PERMISSIONS`、`API_PERMISSIONS` 4 个常量写死在文件中,新增角色或权限点需改代码。
- **违反规则**:项目规则"可扩展性:采用配置驱动设计"。
- **后果**:与 RBAC 模块的 `DATA_SCOPE_RULES` 配置驱动不一致;新增路由时易遗漏权限配置。
#### 问题 37onboarding 流程不支持配置驱动
- **位置**[onboarding-stepper.tsx](file:///e:/Desktop/CICD/src/modules/onboarding/components/onboarding-stepper.tsx)
- **问题**:步骤硬编码 4 步roleConfirm/basicInfo/roleInfo/complete新增"邮箱验证"、"隐私同意"、"家长扫码绑定子女"等步骤需改组件代码。
- **违反规则**:项目规则"可扩展性:采用配置驱动设计,例如通过角色配置决定该模块渲染哪些 Widget/子模块"。
- **后果**:扩展性差;与项目其他模块(如 dashboard widget-configs.ts的配置驱动模式不一致。
### 2.13 数据库与 Schema 一致性P2
#### 问题 38`users` 表混合了认证、用户资料、未成年人保护等多类字段
- **位置**[schema.ts#L27-L57](file:///e:/Desktop/CICD/src/shared/db/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-label`、`aria-invalid`、`aria-describedby`、`role="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.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 细节;⑥ 接入 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.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 个纯函数 + serviceauthorize 仅编排 |
| 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 | 方案 AJWT 仅存 `userId` + `roles`permissions 每次请求通过 `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_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/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/server`2FA 设置页新增 WebAuthn 选项 |
---
## 五、架构图同步说明
本次审计发现架构图(`004_architecture_impact_map.md` 与 `005_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.ts`、`actions.ts`、`schema.ts`、`types.ts`、`services/two-factor-service.ts`(重构后)
- 删除"纯 UI 模块,无 data-access/actions/types"描述
3. **§3 proxy.ts**
- 导出函数列表新增 `resolveDefaultPath`(当前遗漏)
- 依赖列表新增 `shared/lib/route-resolver`(重构后)
4. **`shared/lib` 清单**
- 新增 `route-permissions.ts`、`route-resolver.ts`(重构后)
### 5.3 需删除节点
1. **`shared/components/onboarding-gate.tsx`** 标记为"待删除P0-4",重构后从清单移除。
2. **`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 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 接口定义
```ts
// 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 注入
```tsx
// 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
}
```
页面使用:
```tsx
// 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 拆分
```tsx
// 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
```ts
// 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` 扩展
```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
```ts
// 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 包裹
```tsx
<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 纯函数导出
```ts
// 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 测试修正
```ts
// 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 路由权限配置
```ts
// 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 步骤配置
```ts
// 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.ts` 与 `actions.ts`**
- 迁移 `register/page.tsx` 内联 `registerAction` 到 `modules/auth/actions.ts`
- 新增 `createUser`、`getUserByEmail`、`assignRoleToUser` 到 `data-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#resolveDefaultPath` 与 `onboarding/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.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 后再实施 |