- iOS integration readiness audit (blocker list, module map, deployment check) - CAPI contract for iOS (15 modules, 80+ endpoints) - CAPI error codes and status enums - iOS auth flow, file upload import flow, learning review flow - Web product page nginx fix (reenable longde.cloud) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
20 KiB
iOS 端对接可行性审计报告
审计日期:2026-05-24 | 范围:api-server (NestJS 11) | 方式:只读排查,未修改任何代码
审计人:Claude Code | 服务端:蜂驰云 8核32G (120.53.227.155) + 轻量云 4核4G (10.2.0.7)
A. 总体结论:有条件可以 ✅
在 P0 阻塞项解决之前,不能发布 iOS 对接任务。 P0 涉及 2 个安全/功能严重问题,预计修复工作量 1-2 天。P1 高风险项建议在 iOS 开发第一周内修复。
B. 阻塞项清单
🔴 P0 阻塞 — 不解决不能开始 iOS 对接
| # | 问题 | 位置 | 风险 |
|---|---|---|---|
| 1 | Apple 登录是假的 — APPLE_BUNDLE_ID 未设,退回 mock 模式,任何长度≥4 的字符串都当合法 token |
src/modules/auth/apple-auth.service.ts:28-30,生产 env 缺 APPLE_BUNDLE_ID |
iOS 用户可以用任意字符串登录,伪造 Apple ID |
| 2 | /internal/* 7 个端点零认证暴露公网 — 无 Guard、无 API Key、无 IP 白名单 |
src/common/guards/jwt-auth.guard.ts:31 跳过 internal;internal-rag.controller.ts 有 @Public() 且无 @UseGuards |
攻击者可读取文档原文、写入 chunks/candidates、劫持导入作业 |
🟡 P1 高风险 — 可以开始,但必须尽快修(建议 iOS 开发第一周内)
| # | 问题 | 位置 | 风险 |
|---|---|---|---|
| 3 | 用户 Token 不检查账号状态 — JwtAuthGuard 不解码查库,禁用/已删用户 Token 仍然有效 | src/common/guards/jwt-auth.guard.ts:42-46 |
禁用用户仍可访问所有 API |
| 4 | Refresh Token 不检查用户状态 — 刷新时不验证用户是否被禁用 | src/modules/auth/auth.service.ts:125-128 |
禁用用户可以无限刷新 Token |
| 5 | 用户和管理员 JWT 共用同一密钥 — ADMIN_JWT_ACCESS_SECRET 未设,退回 JWT_SECRET |
src/config/jwt.config.ts:6;生产 env 缺变量 |
降低 Token 隔离性,增加横向移动风险 |
| 6 | 请求 DTO 大量缺失 — iOS 核心流程(import、source、learning-session)用 @Body() body: any |
document-import.controller.ts:13、knowledge-source.controller.ts:18 等 |
iOS 可能发送错误字段,拿到 201 但后续失败 |
🟢 P2 优化项 — 不阻塞 iOS
| # | 问题 | 位置 |
|---|---|---|
| 7 | 无统一分页响应格式 — PaginatedResponse<T> 定义了但从未使用 |
src/common/types/index.ts:9-19 |
| 8 | Swagger 缺少 Response Schema — 所有 @ApiResponse 只有 description,无 typed schema |
全部 CAPI 控制器 |
| 9 | 4 个端点双重包装响应 — logout/feedback/reports/waitlist 返回 {success, data} 被 ResponseInterceptor 再包一层 |
auth.controller.ts:51、feedback.controller.ts:20、content-safety.controller.ts:27、waitlist.controller.ts:19 |
| 10 | Refresh Token 用 SHA-256 而非 bcrypt 哈希 | src/modules/auth/token.service.ts:19,24 |
| 11 | CI/CD 中硬编码了生产数据库密码 | .gitea/workflows/deploy.yml:8,40 |
| 12 | 4核4G 服务器上有旧 MySQL(35 表,dev 密码),但不影响生产 | 10.2.0.7 上 mysql-zhixi 容器 |
C. 服务器与数据库部署
拓扑图
用户 (iOS App)
│
▼
api.longde.cloud (Nginx)
│
▼
蜂驰云 8核32G (120.53.227.155) ← 唯一生产服务器
├── zhixi-api.service NestJS API (端口 3000)
├── zhixi-worker.service BullMQ Worker
├── rag-worker.service Python RAG Worker (端口 8000)
├── Docker: mysql zhixi_prod (90 表) ← 唯一生产数据库
├── Docker: redis 缓存/队列/限流
├── Docker: qdrant 向量数据库
├── Docker: gitea-runner CI/CD Runner
└── Docker: nginx 反向代理
轻量云 4核4G (10.2.0.7) ← 辅助/开发残留
├── Docker: mysql-zhixi zhixi (35 表) ← 旧 dev 数据库,不服务生产
├── Docker: gitea Git 代码仓库
├── Docker: hermes-agent AI Agent
└── zhixi-worker.service 故障中 (auto-restart)
关键结论
| 问题 | 结论 |
|---|---|
| 是否存在数据分裂风险? | 否。 生产仅一套 MySQL zhixi_prod,运行在蜂驰云 |
| iOS 是否会连错数据库? | 否。 API 服务固定连接 zhixi_prod,通过 env 文件配置 |
| 4核4G 的 MySQL 服务生产流量吗? | 否。 数据库名不同(zhixi vs zhixi_prod),仅 35 张旧表,密码是 dev 环境密码 |
| JWT 用户/管理员密钥隔离吗? | 否。 ADMIN_JWT_ACCESS_SECRET 未设,退回与用户相同的 JWT_SECRET |
生产环境配置概况
配置文件:/opt/zhixi/env/.env.production(28 行)
| 配置项 | 状态 | 说明 |
|---|---|---|
| DATABASE_URL | ✅ 已设 | mysql://zhixi_user:***@127.0.0.1:3306/zhixi_prod |
| REDIS_HOST/PORT/PASSWORD | ✅ 已设 | 127.0.0.1:6379 |
| QDRANT_URL | ✅ 已设 | http://127.0.0.1:6333 |
| JWT_SECRET | ✅ 已设 | 64 位 hex |
| ADMIN_JWT_ACCESS_SECRET | ❌ 未设 | 退回 JWT_SECRET |
| APPLE_BUNDLE_ID | ❌ 未设 | 导致 Apple 登录退回 mock |
| ENABLE_SWAGGER | ✅ true | 已恢复,Basic Auth 保护 |
| STORAGE_COS_* | ✅ 已设 | 腾讯云 COS (zhixi-1259685406, ap-beijing) |
| AI_PROVIDER / DEEPSEEK_API_KEY | ✅ 已设 | DeepSeek |
| SILICONFLOW_API_KEY | ✅ 已设 | SiliconFlow (备用) |
D. 各问题详细诊断
P0-1: Apple 登录 Mock 模式
文件: src/modules/auth/apple-auth.service.ts
问题代码(第 23-32 行):
async verifyIdentityToken(identityToken: string): Promise<...> {
if (!this.appleBundleId) {
return this.verifyMock(identityToken); // ← 生产走这分支
}
return this.verifyReal(identityToken);
}
Mock 逻辑(第 34-47 行):
private verifyMock(identityToken: string): { appleUserId: string } {
if (!identityToken || identityToken.trim().length < 4) {
throw new UnauthorizedException('identityToken 无效');
}
return {
appleUserId: crypto.createHash('sha256')
.update(`apple-mock:${identityToken}`)
.digest('hex')
.slice(0, 64),
};
}
真实验证逻辑(第 49-78 行) — 已实现但未启用:
- 使用
jose.jwtVerify验证 JWT 签名 - 通过
createRemoteJWKSet获取 Apple 公钥 (https://appleid.apple.com/auth/keys) - 验证
issuer(Apple)和audience(Bundle ID)
当前状态: 配置(src/config/apple.config.ts)中 bundleId 默认为空字符串。生产 env 未设 APPLE_BUNDLE_ID。任何人发送任意 ≥4 字符的字符串即可登录。
修复方式:
- 在 Apple Developer Console 获取应用的 Bundle ID
- 在生产 env 添加
APPLE_BUNDLE_ID=com.longde.zhixi(或实际值) - 建议同时在代码层加保护:当
appleBundleId为空时直接拒绝而非退回 mock
P0-2: /internal/* 零认证暴露
文件: src/common/guards/jwt-auth.guard.ts:31 + src/modules/rag/internal-rag.controller.ts
JwtAuthGuard 跳过 internal 路由:
if (request.path.startsWith('/admin-api') || request.path.startsWith('/internal')) {
return true; // ← 完全放行,不做任何认证
}
控制器层: InternalRagController 有 @Public() 装饰器,无 @UseGuards()。
暴露的端点(全部无需认证,实测确认):
| 方法 | 路径 | 功能 | 实测 HTTP 状态 |
|---|---|---|---|
| GET | /internal/rag/jobs/next |
获取下一个待处理导入作业(含文档原文) | 200 |
| GET | /internal/rag/jobs/:id |
获取指定作业详情(含原文+元数据) | 200 |
| POST | /internal/rag/jobs/:id/claim |
认领作业(任意 workerId) | 可访问 |
| POST | /internal/rag/jobs/:id/heartbeat |
发送心跳保持作业租约 | 可访问 |
| POST | /internal/rag/jobs/:id/status |
更新作业状态/进度/错误 | 可访问 |
| POST | /internal/rag/chunks |
批量写入 knowledge_chunk 表 | 可访问 |
| POST | /internal/rag/candidates |
批量写入导入候选 | 可访问 |
攻击面:
- 读取所有导入文档的原文(数据泄露)
- 往
knowledge_chunk表注入任意数据(RAG 投毒) - 认领/劫持作业,干扰合法 Worker(拒绝服务)
- 无速率限制,可无限写入
修复方式:
- 方案 A:添加 InternalAuthGuard,验证
X-Internal-API-Key请求头(已有RAG_WORKER_SECRET在生产 env 中) - 方案 B:在 Nginx 层限制 IP(仅允许 127.0.0.1),因为这些端点仅供本地 Worker 使用
- 建议:A+B 组合
P1-3: JwtAuthGuard 不检查用户状态
文件: src/common/guards/jwt-auth.guard.ts:42-46
// 当前逻辑:只验证 JWT 签名,不检查用户状态
const payload = await this.jwtService.verifyAsync(token, {
secret: this.configService.get<string>('jwt.secret'),
});
request.user = { id: String(payload.sub), email: payload.email, role: payload.role };
return true; // ← 禁用的用户、已删除的用户 Token 都通过
对比 AdminAuthGuard(正确示范,admin-auth.guard.ts:45-59):
const adminUser = await this.prisma.adminUser.findUnique({
where: { id: payload.sub },
});
if (!adminUser || adminUser.deletedAt) {
throw new UnauthorizedException('管理员账号不存在');
}
if (adminUser.status !== 'ACTIVE') {
throw new UnauthorizedException('管理员账号已被禁用');
}
if (adminUser.lockedUntil && new Date(adminUser.lockedUntil) > new Date()) {
throw new UnauthorizedException('管理员账号已被锁定,请稍后再试');
}
修复方式: JwtAuthGuard 添加数据库查询,验证 user.deletedAt === null && user.status === 'active'。
P1-4: Refresh Token 不检查用户状态
文件: src/modules/auth/auth.service.ts:125-128
// 刷新时不检查用户状态
const accessToken = await this.tokenService.generateAccessToken(stored.user);
刷新流程完整链路:验证 refreshToken → 撤销旧 token → 生成新 token → 未检查 stored.user 是否被禁用
修复方式: 在生成新 access token 前添加 if (stored.user.status !== 'active' || stored.user.deletedAt) throw new UnauthorizedException()
P1-5: 用户/管理员 JWT 共用密钥
文件: src/config/jwt.config.ts:6
const adminSecret = process.env.ADMIN_JWT_ACCESS_SECRET || process.env.JWT_SECRET;
生产 env 仅设 JWT_SECRET,未设 ADMIN_JWT_ACCESS_SECRET。用户和管理员 Token 用同一密钥签名。
Token 隔离分析:
| 场景 | 结果 |
|---|---|
管理员 Token 访问 /api/* |
JwtAuthGuard 验证通过(同密钥),管理员可以访问用户 API |
用户 Token 访问 /admin-api/* |
AdminAuthGuard 发现 payload.type !== 'admin' 拒绝 |
| 用户 Token 的 payload | { sub, email, role } — 无 type 字段 |
| 管理员 Token 的 payload | { sub, type: 'admin', role, sessionId } — 有 type 字段 |
当前状态: AdminAuthGuard 通过 type 字段区分,即使密钥相同也能阻止用户 Token 访问管理员接口。但反之不行——管理员 Token 可以访问用户接口。且如果密钥泄露,攻击者可伪造管理员 Token。
修复方式: 在生产 env 添加 ADMIN_JWT_ACCESS_SECRET=<独立密钥>。
P1-6: iOS 核心流程缺少 DTO
受影响的端点:
| 端点 | 文件:行号 | 当前状态 |
|---|---|---|
POST /api/imports |
document-import.controller.ts:13 |
@Body() body: any |
POST /api/knowledge-bases/:kbId/sources |
knowledge-source.controller.ts:18 |
@Body() dto: any |
POST /api/learning-sessions |
learning-session.controller.ts |
@Body() body: any |
POST /api/active-recalls/:id/submit |
active-recall.controller.ts |
@Body() body: any |
POST /api/focus-items |
focus-items.controller.ts |
@Body() body: any |
影响: iOS 发送错误字段时,由于无 class-validator 校验,可能拿到 201 Created,直到 AI 处理阶段才失败,调试成本高。
修复方式: 为上述端点创建 DTO 类,添加 @IsString(), @IsOptional(), @IsEnum() 等装饰器。
E. CAPI 契约检查
响应格式
统一成功响应(ResponseInterceptor 全局拦截):
{
"success": true,
"data": <原始返回值>,
"timestamp": "2026-05-24T..."
}
统一错误响应(GlobalExceptionFilter):
{
"success": false,
"statusCode": 401,
"message": "请先登录"
}
已知异常: 4 个端点手动返回 {success, data} 结构,被拦截器二次包装导致 data.data 嵌套:
POST /api/auth/logoutPOST /api/feedbackPOST /api/reportsPOST /api/waitlist
分页
PaginatedResponse<T> 接口已定义(src/common/types/index.ts:9-19),但从未被任何控制器使用。CAPI 分页端点各自返回 raw 格式,iOS 需按端点适配。
Swagger 质量
- ✅ 所有 CAPI 控制器有
@ApiTags(中文标签) - ✅ 所有端点有
@ApiOperation(中文描述) - ❌ 无
@ApiResponse({ type: SomeDto })—— Swagger 无 response schema,iOS 客户端生成工具无法推断返回类型 - ✅ Bearer Auth 已全局配置
- ✅ 生产环境可通过 Basic Auth 访问
/api-docs-json
状态枚举
Prisma schema 使用 String 类型(无原生 enum),状态值在应用层约定。以下是 CAPI 相关模型的状态枚举:
| 模型 | 字段 | 可能的值 |
|---|---|---|
| User | status | active, disabled, deleted |
| User | role | USER, ADMIN |
| KnowledgeBase | status | active, archived, deleted |
| KnowledgeItem | status | active, archived |
| KnowledgeItem | itemType | note, flashcard, concept |
| KnowledgeSource | parseStatus | pending, parsing, completed, failed |
| KnowledgeSource | indexStatus | pending, indexing, completed, failed |
| KnowledgeSource | learningStatus | pending, learning, completed, skipped |
| DocumentImport | status | QUEUED, CLAIMED, PARSING, COMPLETED, FAILED, FAILED_FINAL (另有中间状态 10+ 个) |
| ImportCandidate | status | PENDING, ACCEPTED, REJECTED |
| LearningSession | status | active, completed, cancelled |
| LearningSession | mode | active_recall, feynman, review, reading |
| AiAnalysisJob | status | pending, processing, completed, failed |
| AiAnalysisJob | jobType | active_recall_analysis, feynman_evaluation |
| ReviewCard | status | active, completed, archived |
| ReviewCard | scheduleState | new, learning, review, relearning |
| ReviewCard | difficulty | 数值 (0.0-1.0, SM-2 算法) |
| FocusItem | status | open, completed |
| FocusItem | priority | high, normal, low |
| Feedback | status | pending, reviewed, resolved, closed |
| Feedback | type | bug, feature, general |
F. 文件上传与导入流程(iOS 适配分析)
完整流程图
iOS App API Server COS (腾讯云) BullMQ Worker
======= ========== ============ =============
1. POST /api/files/upload-url
{ filename, mimeType, sizeBytes }
──────────────────────────► 校验文件类型(9种)/大小(≤20MB)
生成预签名 PUT URL
◄────────────────────────── { uploadUrl, objectKey, expiresIn }
2. PUT <uploadUrl>
[二进制文件数据]
───────────────────────────────────────────────────────► 文件存入 COS
3. POST /api/files/complete
{ objectKey, checksum? }
──────────────────────────► 验证文件已到达 COS
创建 UploadedFile 记录
◄────────────────────────── { id, filename, sizeBytes }
4. POST /api/knowledge-bases/:kbId/sources
{ fileId, title, originalFilename }
──────────────────────────► 创建 KnowledgeSource
自动创建 DocumentImport (QUEUED)
◄────────────────────────── { id, parseStatus: "pending" }
5. GET /api/imports/:id/status (轮询)
──────────────────────────► 查 Redis 实时状态 → 回退 DB
◄────────────────────────── { status: "QUEUED", progress: 0 }
... Worker 处理中 ...
6. GET /api/imports/:id/status (轮询)
◄────────────────────────── { status: "COMPLETED", progress: 100,
message: "成功提取 N 个知识点" }
允许的文件类型
| 类型 | MIME |
|---|---|
application/pdf |
|
| TXT | text/plain |
| Markdown | text/markdown, text/x-markdown |
| CSV | text/csv |
| Word | application/vnd.openxmlformats-officedocument.wordprocessingml.document |
| Excel | application/vnd.openxmlformats-officedocument.spreadsheetml.sheet |
| PNG | image/png |
| JPEG | image/jpeg |
| WebP | image/webp |
文件大小上限:20MB,全局 StrictValidationPipe 限制请求体 10MB。
iOS 适配评估
| 评估项 | 结论 | 说明 |
|---|---|---|
| 预签名 URL 直传 | ✅ 可用 | 标准 HTTP PUT,iOS URLSession/Alamofire 均可 |
| 请求/响应格式 | ✅ 可用 | 标准 JSON,字段简单 |
| 认证方式 | ✅ 可用 | 标准 JWT Bearer Token |
| 分块上传/续传 | ❌ 不支持 | 仅支持完整文件一次 PUT,大文件+弱网可能失败 |
| Checksum 校验 | ⚠️ 可选 | CompleteUploadDto.checksum 可选,建议 iOS 传 SHA-256 |
| 错误信息中文 | ⚠️ 注意 | 文件名校验等错误信息为中文,iOS 需处理或本地化 |
| 导入状态 DTO | ❌ 缺失 | POST /api/imports 使用 any,无验证 |
G. CAPI 端点认证测试(生产环境实测)
| 端点 | 无 Token | 结论 |
|---|---|---|
GET /health |
200 | 公开 |
GET /api/users/me |
401 "请先登录" | 受保护 ✅ |
GET /api/knowledge-bases |
401 "请先登录" | 受保护 ✅ |
GET /api/activity/streak |
401 "请先登录" | 受保护 ✅ |
GET /api/reviews/due |
401 "请先登录" | 受保护 ✅ |
GET /api/notifications |
401 "请先登录" | 受保护 ✅ |
GET /api/workspace/dashboard |
401 "请先登录" | 受保护 ✅ |
GET /internal/rag/jobs/next |
200 | 未保护 🔴 |
POST /api/auth/dev-login |
403 "生产环境禁用" | 已禁用 ✅ |
GET /api-docs-json |
401 "Authentication required" | Basic Auth ✅ |
GET /api-docs-json (with auth) |
200 | 正常 ✅ |
H. 给 iOS 团队的接口文档清单
以下文档需要在 iOS 对接开始前编写交付:
| 文档 | 内容 | 优先级 |
|---|---|---|
capi-contract-for-ios.md |
完整 CAPI 端点清单、请求/响应格式、认证方式、通用约定 | P0 |
ios-auth-flow.md |
Apple 登录流程、Token 刷新策略、Keychain 存储建议、登出 | P0 |
ios-file-upload-import-flow.md |
预签名 URL → COS 直传 → Source 创建 → 导入轮询 | P1 |
ios-learning-review-flow.md |
学习会话 → 主动回忆 → AI 分析 → SM-2 复习 → 薄弱项 | P1 |
capi-error-codes-and-status-enums.md |
HTTP 错误码、业务错误消息、所有模型状态枚举值 | P1 |
I. 修复优先级路线图
Week 0 (iOS 启动前)
├── P0-1: 配置 APPLE_BUNDLE_ID,启用真实验证
├── P0-2: 添加 InternalAuthGuard 或 Nginx IP 限制
└── P1-5: 配置 ADMIN_JWT_ACCESS_SECRET(独立密钥)
Week 1 (iOS 开发启动)
├── P1-3: JwtAuthGuard 添加用户状态检查
├── P1-4: Refresh Token 添加用户状态检查
├── P1-6: 为核心端点创建 DTO
└── 编写 iOS 接口文档(5 份)
Week 2+
├── P2-7: 统一分页响应格式
├── P2-8: Swagger 添加 Response Schema
├── P2-9: 修复双重响应包装
└── P2-10/11/12: 杂项优化