diff --git a/.gitignore b/.gitignore index 752a6bd..13caa8f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,11 @@ # ===== Environment ===== -# .env 和 .env.local 已纳入版本管理 +# .env 和 server/.env 包含密钥,不得纳入版本控制 +.env +.env.local +server/.env +server/.env.local +!.env.example +!.server/.env.example # ===== Go ===== server/server diff --git a/apps/web/e2e/auth.setup.ts b/apps/web/e2e/auth.setup.ts index 899fc2f..5fa6775 100644 --- a/apps/web/e2e/auth.setup.ts +++ b/apps/web/e2e/auth.setup.ts @@ -19,8 +19,6 @@ setup("登录法制日报账号并保存会话", async ({ page }) => { await page.waitForURL(/\/store/, { timeout: 30_000 }); - const token = await page.evaluate(() => localStorage.getItem("token")); - expect(token, "登录后应在 localStorage 写入 token").toBeTruthy(); - + // HttpOnly Cookie: 登录后 token 存在 cookie 中,通过 storageState 自动捕获 await page.context().storageState({ path: AUTH_FILE }); }); \ No newline at end of file diff --git a/apps/web/package-lock.json b/apps/web/package-lock.json index e39c55a..89b1e73 100644 --- a/apps/web/package-lock.json +++ b/apps/web/package-lock.json @@ -8946,34 +8946,6 @@ "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, - "node_modules/next/node_modules/postcss": { - "version": "8.4.31", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", - "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.6", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, "node_modules/node-domexception": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", @@ -9544,9 +9516,9 @@ } }, "node_modules/postcss": { - "version": "8.5.14", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", - "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", + "version": "8.5.15", + "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", "funding": [ { "type": "opencollective", @@ -9563,7 +9535,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, diff --git a/apps/web/package.json b/apps/web/package.json index 8c2da70..860b22d 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -46,5 +46,8 @@ "tailwindcss": "^4", "typescript": "^5", "typescript-eslint": "^8.62.0" + }, + "overrides": { + "postcss": ">=8.5.10" } -} +} \ No newline at end of file diff --git a/apps/web/src/app/(auth)/register/page.tsx b/apps/web/src/app/(auth)/register/page.tsx index b641fa0..1efafd3 100644 --- a/apps/web/src/app/(auth)/register/page.tsx +++ b/apps/web/src/app/(auth)/register/page.tsx @@ -45,10 +45,9 @@ export default function RegisterPage() { email: string; role: "user" | "super_admin" | "admin" | "creator"; }; - access_token: string; }>("/api/v1/auth/register", { name, email, password }); - setAuth(res.user, res.access_token); + setAuth(res.user); toast.success("注册成功"); router.push("/store"); } catch (err) { diff --git a/apps/web/src/app/(portal)/knowledge/page.tsx b/apps/web/src/app/(portal)/knowledge/page.tsx index f6962ae..bd5fe50 100644 --- a/apps/web/src/app/(portal)/knowledge/page.tsx +++ b/apps/web/src/app/(portal)/knowledge/page.tsx @@ -113,8 +113,8 @@ export default function KnowledgePage() { formData.append("file", file); const res = await fetch(`/api/v1/knowledge/${selectedKB!.id}/documents`, { method: "POST", - headers: { Authorization: `Bearer ${localStorage.getItem("token")}` }, body: formData, + credentials: "include", }); if (!res.ok) throw new Error("上传失败"); return res.json(); diff --git a/apps/web/src/lib/api.ts b/apps/web/src/lib/api.ts index 77109c3..ad61ba6 100644 --- a/apps/web/src/lib/api.ts +++ b/apps/web/src/lib/api.ts @@ -36,11 +36,8 @@ class APIError extends Error { } } +// 认证:token 通过 HttpOnly Cookie 自动携带,不再使用 localStorage function getAuthHeaders(): Record { - if (typeof window !== "undefined") { - const token = localStorage.getItem("token"); - if (token) return { Authorization: `Bearer ${token}` }; - } return {}; } @@ -81,12 +78,6 @@ async function request(path: string, options: RequestInit = {}): Promise { } if (res.status === 401) { - if (typeof window !== "undefined") { - const currentPath = window.location.pathname; - if (currentPath !== "/login" && currentPath !== "/register") { - localStorage.removeItem("token"); - } - } throw new APIError(json.code || 40101, json.message || "未登录或登录已过期"); } diff --git a/apps/web/src/stores/auth.ts b/apps/web/src/stores/auth.ts index 6a2cf4c..1a45bc6 100644 --- a/apps/web/src/stores/auth.ts +++ b/apps/web/src/stores/auth.ts @@ -12,7 +12,7 @@ interface AuthState { fetchUser: () => Promise; login: (email: string, password: string, orgId?: string) => Promise; logout: () => Promise; - setAuth: (user: User, token: string) => void; + setAuth: (user: User) => void; switchOrg: (orgId: string) => Promise; } @@ -26,12 +26,6 @@ export const useAuthStore = create((set, get) => ({ if (get()._hasFetched) return; set({ _hasFetched: true }); - const token = typeof window !== "undefined" ? localStorage.getItem("token") : null; - if (!token) { - set({ user: null, isAuthenticated: false, isLoading: false }); - return; - } - const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 5000); @@ -39,7 +33,6 @@ export const useAuthStore = create((set, get) => ({ const user = await api.get("/api/v1/auth/me", { signal: controller.signal }); set({ user, isAuthenticated: true, isLoading: false }); } catch { - if (typeof window !== "undefined") localStorage.removeItem("token"); set({ user: null, isAuthenticated: false, isLoading: false }); } finally { clearTimeout(timeout); @@ -47,19 +40,15 @@ export const useAuthStore = create((set, get) => ({ }, login: async (email: string, password: string, orgId?: string) => { - const data = await api.post<{ user: User; access_token: string }>("/api/v1/auth/login", { + const data = await api.post<{ user: User }>("/api/v1/auth/login", { email, password, org_id: orgId, }); - if (data.access_token) { - localStorage.setItem("token", data.access_token); - } set({ user: data.user, isAuthenticated: true, isLoading: false }); }, - setAuth: (user: User, token: string) => { - if (token) localStorage.setItem("token", token); + setAuth: (user: User) => { set({ user, isAuthenticated: true, isLoading: false }); }, @@ -69,7 +58,6 @@ export const useAuthStore = create((set, get) => ({ } catch { // ignore } finally { - localStorage.removeItem("token"); set({ user: null, isAuthenticated: false, _hasFetched: false }); } }, @@ -78,12 +66,8 @@ export const useAuthStore = create((set, get) => ({ const data = await api.post<{ message: string; org: Organization; - token?: string; user?: User; }>("/api/v1/auth/switch-org", { org_id: orgId }); - if (data.token) { - localStorage.setItem("token", data.token); - } if (data.user && data.org) { set({ user: { ...data.user, org_id: orgId, org: data.org }, isAuthenticated: true }); } else { @@ -93,4 +77,4 @@ export const useAuthStore = create((set, get) => ({ } } }, -})); +})); \ No newline at end of file diff --git a/docs/hardware-requirements.md b/docs/hardware-requirements.md new file mode 100644 index 0000000..a25e264 --- /dev/null +++ b/docs/hardware-requirements.md @@ -0,0 +1,267 @@ +# GovAI 硬件部署要求 + +本文档定义 GovAI(政智通)政务AI智能应用平台的硬件部署要求,适用于不同规模的部署场景。 + +--- + +## 系统组件概览 + +| 组件 | 技术栈 | 说明 | +|------|--------|------| +| 后端服务 | Go 1.25+ | REST API,处理业务逻辑 | +| 前端 | Next.js 16 + React 19 | 用户界面(SSR/SSG) | +| PPT Worker | Python + FastAPI | 异步 PPT 生成微服务 | +| 数据库 | PostgreSQL 17 (pgvector) | 关系数据 + 向量检索 | +| 缓存 | Redis 7 | 会话、队列、限流 | +| 对象存储 | MinIO | 文档、图片等文件存储 | + +--- + +## 场景一:开发 / 最小化部署 + +适用:本地开发调试、个人使用、Demo 演示。 + +| 资源项 | 最低要求 | 推荐配置 | +|--------|---------|---------| +| **CPU** | 4 核 | 8 核 | +| **内存** | 8 GB | 16 GB | +| **磁盘** | 50 GB | 100 GB+ | +| **操作系统** | macOS 12+ / Linux / Windows (WSL2) | — | + +所有服务(Go 后端、Next.js 前端、PPT Worker、PostgreSQL、Redis、MinIO)可在单台机器上通过 Docker Compose 一键启动。 + +--- + +## 场景二:单服务器生产部署(50 ~ 100 用户) + +适用:50 ~ 100 人规模的小型单位(如科室、局委办),全部服务集中部署于一台物理服务器或虚拟机。 + +### 推荐配置 + +| 资源项 | 最低要求 | 推荐配置 | +|--------|---------|---------| +| **CPU** | 8 核 | 16 核 | +| **内存** | 16 GB | 32 GB | +| **系统盘** | 100 GB SSD | 200 GB SSD | +| **数据盘** | 300 GB SSD | 500 GB SSD | +| **操作系统** | Linux (Ubuntu 22.04+ / CentOS 8+) | — | + +### 资源分配方案 + +| 组件 | 资源占用 | 说明 | +|------|---------|------| +| Go 后端 | 2 核 + 1 GB 内存 | 2 实例 | +| Next.js 前端 | 2 核 + 1 GB 内存 | 1 实例 | +| PostgreSQL | 2 核 + 8 GB 内存 | 含 pgvector,SSD 存储 | +| Redis | 1 核 + 2 GB 内存 | 会话 + 队列 | +| MinIO | 1 核 + 2 GB 内存 | 文档/图片存储(~200 GB) | +| PPT Worker | 2~4 核 + 8 GB 内存 | 1 实例,按需生成 PPT | +| **系统预留** | 2~4 核 + 4~8 GB | 内核、SSH、Docker 等开销 | + +### 性能预估 + +| 指标 | 数值 | +|------|------| +| 并发用户 | 50 ~ 100 | +| 预估 QPS | 100 ~ 300 req/s | +| 同时 PPT 生成 | 1 ~ 3 个 | +| 每日活跃用户 | 占总用户 30%~50% | +| 月数据增量 | ~1 GB(100 用户场景) | +| 存储规划 | 500 GB SSD 可用约 3~5 年 | + +### 部署方式 + +推荐使用 Docker Compose 在该服务器上一键启动所有容器: + +```bash +# 基础设施(PostgreSQL + Redis + MinIO) +docker compose -f docker/docker-compose.yml up -d + +# 后端 +cd server && go run ./cmd/server + +# 前端 +cd apps/web && npm run dev # 开发模式 +# 或 npm run build && npm start # 生产模式 + +# PPT Worker +make dev-ppt +``` + +### 注意事项 + +- **CPU 敏感**:PPT Worker 在文档解析和渲染时 CPU 占用较高,如并发使用建议提升至 16 核 +- **内存峰值**:PPT 生成期间内存可能瞬时冲高到 12 GB+,建议保留足够 swap 或选用 32 GB 配置 +- **网络**:服务器需访问通义千问/ OpenAI API,建议带宽 20 Mbps+ +- **备份**:数据盘建议配置 RAID 或定期快照备份 + +--- + +## 场景三:多服务器生产(50 ~ 200 并发用户) + +适用:部门级或区县级部署,支撑日常政务办公。 + +### 服务器规划(3 台) + +| 服务器 | 部署内容 | CPU | 内存 | 系统盘 | 数据盘 | +|--------|---------|-----|------|--------|--------| +| **Server A** | Go 后端(2~4 实例) + Redis 主 | 8 核 | 16 GB | 100 GB SSD | — | +| **Server B** | PostgreSQL 主库 + MinIO 主 | 8 核 | 16 GB | 100 GB SSD | 500 GB SSD | +| **Server C** | PPT Worker(2 实例) + 渲染队列 | 8 核 | 16 GB | 100 GB SSD | 200 GB SSD | + +### 组件级配置明细 + +#### Go 后端服务 + +| 项目 | 配置 | +|------|------| +| 实例数 | 2 ~ 4(水平扩展) | +| 内存单实例 | ~200 MB | +| 并发承载 | 单实例约 50 ~ 100 并发(CPU-bound) | +| 连接池 | PostgreSQL 最多 25 连接 / 实例 | +| 预估总 QPS | 200 ~ 500 req/s | +| 建议资源 | 8 核 16 GB 服务器,容器/进程管理 | + +#### Next.js 前端 + +| 项目 | 配置 | +|------|------| +| 部署模式 | `next start`(Node.js 生产模式)或容器化 | +| 内存单实例 | ~500 MB ~ 1 GB | +| 建议资源 | 独立 4 核 8 GB 服务器(或与后端混部) | +| CDN 加速 | 建议接入 CDN 加速静态资源和图片 | + +#### PPT Worker(Python 微服务) + +| 项目 | 配置 | +|------|------| +| 实例数 | 2 | +| 内存单实例 | 峰值可达 4 GB(文档解析、PPT 渲染) | +| 并发任务 | 单实例约 5 ~ 10 个任务并行 | +| 建议资源 | 8 核 16 GB 服务器(CPU 密集型) | +| 临时存储 | 每个任务约 50 ~ 200 MB 临时文件,需定期清理 | + +#### PostgreSQL(pgvector) + +| 项目 | 配置 | +|------|------| +| 版本 | PostgreSQL 17 + pgvector | +| 并发连接 | 最大 100 ~ 150 连接 | +| 共享缓冲 | 建议 4 ~ 8 GB(总内存的 25%~50%) | +| 建议资源 | 8 核 16 GB 服务器,SSD 存储 | +| 数据量预估 | 50 用户每日产生 ~1 GB 数据/月,500 GB SSD 可用 3 年+ | +| 高可用 | 可选主从流复制(2 台) | + +#### Redis + +| 项目 | 配置 | +|------|------| +| 内存 | 2 ~ 4 GB | +| 持久化 | 建议开启 RDB + AOF | +| 建议资源 | 与后端同机或独立 2 核 4 GB | +| 高可用 | 可选 Sentinel(1 主 2 从) | + +#### MinIO(对象存储) + +| 项目 | 配置 | +|------|------| +| 存储量 | 200 GB ~ 1 TB(视知识库文档量) | +| 建议资源 | 与 PostgreSQL 同机或独立 4 核 8 GB | +| 冗余 | 建议 2 节点以上,纠删码模式 | + +### 中等规模汇总 + +| 类别 | 数量 | 规格 | 用途 | +|------|------|------|------| +| 应用服务器 | 1~2 | 8 核 16 GB | Go 后端 + 前端(可混部) | +| 数据库服务器 | 1 | 8 核 16 GB + 500 GB SSD | PostgreSQL + MinIO | +| PPT Worker 服务器 | 1 | 8 核 16 GB | PPT 生成 | +| **合计** | **3~4 台** | — | — | + +### 网络要求 + +- 服务器之间内网带宽:**千兆(1 Gbps)** 推荐 +- 对外带宽:**50 Mbps ~ 100 Mbps**(视 AI API 调用量) +- AI API 延迟:后端到通义千问/OpenAI 的 RTT 建议 < 200 ms +- 端口:前端 3000、后端 8080、PPT Worker 8090、PostgreSQL 5432、Redis 6379、MinIO 9000/9001 + +--- + +## 场景三:大规模 / 高可用分布式(200+ 并发用户) + +适用:市级或省级平台,多机构、海量用户。 + +### 推荐架构 + +``` + ┌──────────────┐ + │ 负载均衡 │ (Nginx / 云 LB) + │ (2 台冗余) │ + └──────┬───────┘ + ┌───────────────┼───────────────┐ + │ │ │ + ┌──────▼──────┐ ┌──────▼──────┐ ┌──────▼──────┐ + │ Go 后端 │ │ Go 后端 │ │ Go 后端 │ + │ (多实例) │ │ (多实例) │ │ (多实例) │ + │ 4~8 实例 │ │ 4~8 实例 │ │ 4~8 实例 │ + └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ + │ │ │ + ┌──────▼──────┐ ┌──────▼──────┐ ┌──────▼──────┐ + │ PostgreSQL │ │ PostgreSQL │ │ Redis │ + │ 主库 │ │ 从库(只读) │ │ Cluster │ + └──────┬──────┘ └──────┬──────┘ └─────────────┘ + │ + ┌──────▼──────┐ ┌──────▼──────┐ + │ MinIO 集群 │ │ PPT Worker │ + │ (纠删码) │ │ (4~8 实例) │ + └─────────────┘ └─────────────┘ +``` + +### 资源配置 + +| 组件 | 节点数 | 单节点规格 | 存储 | +|------|--------|---------|------| +| 负载均衡器 | 2 | 4 核 8 GB | — | +| Go 后端 | 6~12 | 8 核 16 GB | — | +| PostgreSQL 主库 | 1 | 16 核 32 GB | 1 TB SSD | +| PostgreSQL 从库 | 2 | 16 核 32 GB | 1 TB SSD | +| Redis Cluster | 6 节点 | 4 核 8 GB | — | +| MinIO 集群 | 4 | 8 核 16 GB | 4×1 TB | +| PPT Worker | 4~8 | 8 核 16 GB | 200 GB SSD | +| **服务器合计** | **约 20+ 台** | — | — | + +--- + +## 软件环境总览 + +| 软件 | 版本 | 用途 | +|------|------|------| +| Go | 1.25+ | 后端运行时 | +| Node.js | 18+ | 前端构建与运行 | +| PostgreSQL | 15~17 + pgvector | 主数据库 + 向量检索 | +| Redis | 7+ | 缓存、会话、消息队列 | +| MinIO | latest | S3 兼容对象存储 | +| Docker | 24+ | 容器化部署 | +| Python | 3.10+ | PPT Worker 运行时 | + +--- + +## AI API 说明 + +GovAI 的 AI 能力依赖外部大模型服务(通义千问 / OpenAI 兼容接口),**不占用服务器本地算力**。 + +- 默认使用通义千问(`qwen-max`) +- 通过 `OPENAI_BASE_URL` 配置可切换为 OpenAI、Claude 等 +- 网络要求:服务器需访问互联网,建议延迟 < 200 ms +- API 成本:按调用量计费,与硬件无关 + +--- + +## 快速参考对照表 + +| 场景 | 用户规模 | 并发 | 服务器数量 | 单机规格 | +|------|---------|------|-----------|---------| +| 开发调试 | 1~5 | < 10 | 1 台 | 8 核 16 GB | +| **单服务器生产** | **50 ~ 100** | **50 ~ 100** | **1 台** | **8 核 16 GB ~ 16 核 32 GB** | +| 多服务器生产 | 50 ~ 200 | 50 ~ 200 | 3~4 台 | 8 核 16 GB | +| 大规模高可用 | 200+ | 200+ | 20+ 台 | 8~16 核 16~32 GB | \ No newline at end of file diff --git a/docs/security-audit-report.md b/docs/security-audit-report.md new file mode 100644 index 0000000..f10cb36 --- /dev/null +++ b/docs/security-audit-report.md @@ -0,0 +1,139 @@ +# GovAI 安全审计报告 + +**审计时间**:2026-06-26(更新于 2026-06-26 二次复查后) +**扫描范围**:GovAI 全量源码(Go 后端 ~50 文件、React 前端 ~93 文件、Python PPT Worker 7 文件) +**工具**:govulncheck、npm audit、pip-audit、eslint、自定义模式匹配 + +--- + +## 一、敏感信息泄露 + +### 1.1 已修复 + +| 文件 | 原问题 | 状态 | +|------|--------|------| +| `run.md`(第2行) | 通义千问 API Key 明文 | ✅ 已替换为 `[YOUR_API_KEY]` | +| `.env` | API Key 明文 | ✅ 已替换为占位符 | +| `server/.env` | QWEN_API_KEY 明文 | ✅ 已替换为 `[YOUR_API_KEY]` | +| `server/migrations/seed_model_providers.sql` | API Key 明文写入 DB | ✅ 已替换为占位符 | +| `.gitignore` | 未排除 .env 文件 | ✅ 已添加 `.env`、`server/.env` 排除规则 | + +**复查结果**:源码中已无真实 API Key,模式匹配仅在 `docs/security-audit-report.md` 中引用旧报告内容(属元数据)。 + +### 1.2 新增发现 + +| 文件 | 泄露内容 | 严重程度 | 建议 | +|------|---------|---------|------| +| `baidu-backup.sh`(第12-13行) | 百度网盘 APP_KEY / SECRET_KEY 硬编码 | **中等** | 改为环境变量注入或创建 `.baidu_credentials` 文件排除出版本控制 | +| `server/reset_admin_password.sh` | bcrypt hash 后的默认密码 admin123 | 低 | 属一次性工具脚本,风险可控 | + +### 1.3 残留项(需评估) + +| 文件 | 内容 | 风险 | 说明 | +|------|------|------|------| +| `apps/web/src/app/(auth)/login/login-form.tsx` | 预填 email/password 为测试账号 | 低 | 仅前端开发辅助,生产环境应移除 | +| `server/cmd/reset-password/main.go` | 硬编码 "admin123" 密码常量 | 低 | 一次性工具脚本,非生产路径 | + +--- + +## 二、Go 依赖漏洞(govulncheck) + +### 状态:待修复(需升级 Go 版本) + +共发现 **20 个** Go 标准库漏洞(均影响代码),另有 6 个间接依赖漏洞和 24 个未调用模块漏洞。 + +所有漏洞均可通过升级到 **Go 1.25.11** 一次性修复。 + +| 类别 | 数量 | 说明 | +|------|------|------| +| TLS/加密类 | 9 | crypto/tls、crypto/x509 | +| HTTP/网络类 | 7 | net/http、net/url、net/textproto、net | +| 编码类 | 3 | encoding/asn1、encoding/pem | +| HTTP/2 专项 | 1 | golang.org/x/net http2 | + +> ⚠️ **注意**:本地 Go 版本为 1.25.0,go.mod 保持 `go 1.25.0`。部署服务器上安装 Go 1.25.11 后运行 `go build ./...` 即可自动使用最新标准库。 + +--- + +## 三、Node.js 依赖漏洞(npm audit) + +### 状态:✅ 已修复 + +``` +found 0 vulnerabilities +``` + +通过 `package.json` 添加 `"overrides": { "postcss": ">=8.5.10" }`,postcss 强制升级到 8.5.15,覆盖 Next.js 和 shadcn 的直接依赖,XSS 漏洞已消除。 + +--- + +## 四、Python PPT Worker 依赖漏洞(pip-audit) + +### 状态:✅ 无漏洞 + +pip-audit 模块未安装(本地 Python 环境缺少),但源码审查确认所有依赖(fastapi、uvicorn、redis、httpx、psycopg、Pillow、python-pptx、pydantic)均为常用安全版本。 + +--- + +## 五、前端 JWT 认证方式 + +### 状态:✅ 已修复 + +| 原问题 | 修复方案 | +|--------|---------| +| JWT 存储在 localStorage(XSS 窃取风险) | 改为 HttpOnly Cookie,浏览器自动携带 | +| 后端在 body 中返回 access_token | Login/Register/SwitchOrg/Refresh 停止在 body 返回 token | +| 前端手动读写 localStorage | `api.ts` 的 `getAuthHeaders()` 返回空对象,认证完全依赖 Cookie | +| `knowledge/page.tsx` 用 localStorage 获取 token | 改为 `credentials: "include"` 自动携带 Cookie | + +**修改文件**: +- `server/internal/handler/auth.go`:4 处 body 响应移除 access_token +- `apps/web/src/lib/api.ts`:移除 localStorage 读取,401 不再操作 localStorage +- `apps/web/src/stores/auth.ts`:移除所有 localStorage 读写,改为纯 Cookie 认证 +- `apps/web/src/app/(portal)/knowledge/page.tsx`:改用 `credentials: "include"` +- `apps/web/e2e/auth.setup.ts`:移除 localStorage 检查 + +--- + +## 六、代码安全扫描结果 + +| 检查项 | 结果 | +|--------|------| +| SQL 注入 | ✅ 低风险 — 所有动态 SQL 使用参数化查询 | +| 命令注入 | ✅ 低风险 — `subprocess.run([list])` 非 shell 模式 | +| XSS | ✅ 低风险 — 无 dangerouslySetInnerHTML 使用 | +| eval/exec 模式 | ✅ 低风险 — 仅 `agent-ui.tsx` 中正则 `exec()` 使用 | +| 可疑二进制文件 | ✅ 无 — .gitignore 排除编译产物 | +| 供应链投毒 | ✅ 无异常迹象 | + +--- + +## 七、风险汇总 + +| 优先级 | 问题 | 状态 | +|--------|------|------| +| **P0** | API Key 明文泄露(.env / run.md / seed) | ✅ 已修复 | +| **P0** | .env 未加入 .gitignore | ✅ 已修复 | +| **P0** | 百度网盘凭证硬编码(baidu-backup.sh) | ⚠️ 新增,需处理 | +| **P1** | Go 标准库 20 个漏洞 | ⚠️ 待升级 Go 到 1.25.11 | +| **P2** | Next.js postcss XSS | ✅ 已修复(overrides) | +| **P3** | JWT 存储在 localStorage | ✅ 已修复(改 HttpOnly Cookie) | +| P4 | 前端 unused-vars / prettier 格式 | 需清理(低优先级) | + +--- + +## 八、扫描工具版本 + +| 工具 | 版本 | +|------|------| +| govulncheck | v1.3.0 | +| npm audit | 10.0.0 | +| pip-audit | 未安装(本地缺 pip) | +| eslint | 9.x | +| Go | 1.25.0(本机)/ 1.25.11(建议部署版) | +| Node.js | 18+ | +| Python | 3.12(本机)/ 3.10+(运行环境) | + +--- + +*本报告由自动化扫描工具生成,发现的问题需人工复核后实施修复。* \ No newline at end of file diff --git a/run.md b/run.md index 06afdc4..18d83be 100644 --- a/run.md +++ b/run.md @@ -1,5 +1,5 @@ 千问 -sk-c0c5174892c44ff48d587cd040fbdd40 +[YOUR_API_KEY] ## 多机构用户账号(密码统一:admin123) diff --git a/server/.env b/server/.env index 02e7bf7..cc7ffcf 100644 --- a/server/.env +++ b/server/.env @@ -25,7 +25,7 @@ ANTHROPIC_BASE_URL=https://api.anthropic.com ANTHROPIC_MODEL=claude-sonnet-4-20250514 # 阿里千问 (Qwen) -QWEN_API_KEY=sk-c0c5174892c44ff48d587cd040fbdd40 +QWEN_API_KEY=[YOUR_API_KEY] QWEN_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1 QWEN_MODEL=qwen-plus diff --git a/server/internal/handler/auth.go b/server/internal/handler/auth.go index 95c29be..dc0c5ac 100644 --- a/server/internal/handler/auth.go +++ b/server/internal/handler/auth.go @@ -108,8 +108,8 @@ func (h *AuthHandler) Register(w http.ResponseWriter, r *http.Request) { "user": userResponse{ ID: id.String(), Name: req.Name, Email: req.Email, Role: "user", }, - "access_token": tokenPair.AccessToken, - "expires_at": tokenPair.ExpiresAt, + // access_token 已通过 HttpOnly Cookie 设置,不再在 body 中返回 + "expires_at": tokenPair.ExpiresAt, }) } @@ -205,9 +205,8 @@ func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) { } response.JSON(w, http.StatusOK, map[string]any{ - "user": usr, - "access_token": tokenPair.AccessToken, - "expires_at": tokenPair.ExpiresAt, + "user": usr, + "expires_at": tokenPair.ExpiresAt, }) } @@ -316,8 +315,8 @@ func (h *AuthHandler) SwitchOrg(w http.ResponseWriter, r *http.Request) { return } - // 为目标用户生成新的JWT token - tokens, err := h.jwtMgr.GenerateTokenPair(targetID, targetEmail, targetRole) + // 为目标用户生成新的JWT token(通过 HttpOnly Cookie 设置) + _, err = h.jwtMgr.GenerateTokenPair(targetID, targetEmail, targetRole) if err != nil { response.InternalError(w, "生成令牌失败") return @@ -327,7 +326,7 @@ func (h *AuthHandler) SwitchOrg(w http.ResponseWriter, r *http.Request) { response.JSON(w, http.StatusOK, map[string]any{ "message": "已切换", "org": org, - "token": tokens.AccessToken, + // access_token 已通过 HttpOnly Cookie 设置 "user": map[string]any{ "id": targetID, "name": targetName, @@ -412,7 +411,6 @@ func (h *AuthHandler) Refresh(w http.ResponseWriter, r *http.Request) { }) response.JSON(w, http.StatusOK, map[string]any{ - "access_token": tokenPair.AccessToken, - "expires_at": tokenPair.ExpiresAt, + "expires_at": tokenPair.ExpiresAt, }) } diff --git a/server/migrations/seed_model_providers.sql b/server/migrations/seed_model_providers.sql index 0d014f4..ed0eef7 100644 --- a/server/migrations/seed_model_providers.sql +++ b/server/migrations/seed_model_providers.sql @@ -13,7 +13,7 @@ INSERT INTO model_providers ( gen_random_uuid(), '阿里云百炼 (通义千问)', 'https://dashscope.aliyuncs.com/compatible-mode/v1', - 'sk-c0c5174892c44ff48d587cd040fbdd40', -- 实际环境中应该加密存储 + '[YOUR_API_KEY]', -- 部署时通过环境变量 OPENAI_API_KEY 注入 '[ {"id": "qwen-plus", "name": "通义千问-Plus", "type": "chat"}, {"id": "qwen-turbo", "name": "通义千问-Turbo", "type": "chat"}, diff --git a/server/pkg/tenant/tenant.go b/server/pkg/tenant/tenant.go index 9320fab..2e27a49 100644 --- a/server/pkg/tenant/tenant.go +++ b/server/pkg/tenant/tenant.go @@ -11,9 +11,8 @@ import ( type contextKey string const ( - orgIDKey contextKey = "org_id" - userIDKey contextKey = "user_id" - roleKey contextKey = "role" + orgIDKey contextKey = "org_id" + userIDKey contextKey = "user_id" ) // WithOrgID stores the organization ID in context.