178 lines
7.1 KiB
Markdown
178 lines
7.1 KiB
Markdown
# 实现方案
|
||
|
||
> 时间戳:2026-04-20-10-14-09
|
||
> 对应需求分析:`需求分析-2026-04-20-10-14-09.md`
|
||
|
||
---
|
||
|
||
## 1. 方案概述
|
||
|
||
在不改变 `storage.get/set` 同步调用签名的前提下,将 `systemSettings` 的底层加密从 XOR 迁移至 Web Crypto API AES-GCM,密钥与 API Key、授权哈希全部移入构建阶段环境变量。应用启动时异步初始化存储模块,预解密 `systemSettings` 到内存缓存,后续同步读写均走缓存。旧 XOR 密文自动识别并迁移。UserManage.tsx 中的明文授权常量替换为 SHA-256 环境变量哈希比对,同时以最小代价替换原生 alert/confirm 为内联状态提示。
|
||
|
||
---
|
||
|
||
## 2. 详细改动清单
|
||
|
||
### 2.1 文件:src/utils/storage.ts
|
||
|
||
| 行号范围 | 改动类型 | 说明 |
|
||
|----------|----------|------|
|
||
| 1-6 | 删除 | 移除 `API_KEY_CODES` 数组与 `getDefaultApiKey()` 的旧实现 |
|
||
| 8 | 修改 | `CRYPTO_KEY` 改为从 `process.env.STORAGE_KEY` 读取,并更名为 `LEGACY_CRYPTO_KEY` |
|
||
| 10-25 | 保留 | 旧 `xorEncrypt` / `xorDecrypt` 保留用于向后兼容读取 |
|
||
| 新增 | 新增 | 引入 Web Crypto API:`importCryptoKey`、`aesEncrypt`、`aesDecrypt` |
|
||
| 新增 | 新增 | `AES_GCM_PREFIX = 'ag:'` 用于识别新格式密文 |
|
||
| 新增 | 新增 | `systemSettingsCache`、`initPromise` 等模块级状态 |
|
||
| 新增 | 新增 | 异步 `init()` 函数:导入密钥、预加载 systemSettings、自动迁移旧数据 |
|
||
| 27-43 | 修改 | `get()` 对 `systemSettings` 改为从内存缓存返回 |
|
||
| 45-55 | 修改 | `set()` 对 `systemSettings` 改为更新缓存并触发异步 `persistSystemSettings()` |
|
||
| 新增 | 新增 | `sha256(text)` 工具函数(Web Crypto API)供 UserManage.tsx 使用 |
|
||
|
||
**关键代码逻辑**:
|
||
```typescript
|
||
// AES-GCM 密文格式: ag:<base64(iv)>:<base64(ciphertext)>
|
||
const AES_GCM_PREFIX = 'ag:';
|
||
|
||
async function init() {
|
||
const keyData = process.env.STORAGE_KEY || '';
|
||
if (!keyData) throw new Error('STORAGE_KEY not configured in build env');
|
||
cryptoKey = await crypto.subtle.importKey(
|
||
'raw',
|
||
new TextEncoder().encode(keyData.padEnd(32, '0').slice(0, 32)),
|
||
{ name: 'AES-GCM' },
|
||
false,
|
||
['encrypt', 'decrypt']
|
||
);
|
||
const raw = localStorage.getItem('systemSettings');
|
||
if (raw) {
|
||
if (raw.startsWith(AES_GCM_PREFIX)) {
|
||
systemSettingsCache = await aesDecrypt(raw);
|
||
} else {
|
||
try { systemSettingsCache = JSON.parse(raw); }
|
||
catch { systemSettingsCache = JSON.parse(xorDecrypt(raw, LEGACY_KEY)); }
|
||
await persistSystemSettings(); // 静默迁移
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### 2.2 文件:src/main.tsx
|
||
|
||
| 行号范围 | 改动类型 | 说明 |
|
||
|----------|----------|------|
|
||
| 4-10 | 修改 | 包装为异步 `bootstrap()`,先 `await storage.init()` 再 render App |
|
||
|
||
```typescript
|
||
import { storage } from './utils/storage';
|
||
async function bootstrap() {
|
||
await storage.init();
|
||
createRoot(document.getElementById('root')!).render(
|
||
<StrictMode><App /></StrictMode>
|
||
);
|
||
}
|
||
bootstrap();
|
||
```
|
||
|
||
### 2.3 文件:vite.config.ts
|
||
|
||
| 行号范围 | 改动类型 | 说明 |
|
||
|----------|----------|------|
|
||
| 10-12 | 修改 | 在 `define` 中新增 4 个环境变量注入 |
|
||
|
||
```typescript
|
||
define: {
|
||
'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY),
|
||
'process.env.STORAGE_KEY': JSON.stringify(env.STORAGE_KEY),
|
||
'process.env.DEFAULT_API_KEY': JSON.stringify(env.DEFAULT_API_KEY),
|
||
'process.env.ADMIN_AUTH_HASH': JSON.stringify(env.ADMIN_AUTH_HASH),
|
||
'process.env.ADMIN_AUTH_SALT': JSON.stringify(env.ADMIN_AUTH_SALT),
|
||
},
|
||
```
|
||
|
||
### 2.4 文件:.env.example
|
||
|
||
| 行号范围 | 改动类型 | 说明 |
|
||
|----------|----------|------|
|
||
| 新增 | 新增 | 追加 4 个必需环境变量模板 |
|
||
|
||
```bash
|
||
# 本地存储 AES-GCM 加密密钥(建议 32 位随机字符串)
|
||
STORAGE_KEY="YourRandom32CharKeyHere!!!"
|
||
|
||
# 默认 AI API Key(替代原 API_KEY_CODES 硬编码)
|
||
DEFAULT_API_KEY="your-api-key-here"
|
||
|
||
# 禁用管理员操作的授权码 SHA-256 哈希值(hex 小写)
|
||
ADMIN_AUTH_HASH="your-sha256-hash-here"
|
||
|
||
# 授权码哈希盐值
|
||
ADMIN_AUTH_SALT="your-random-salt-here"
|
||
```
|
||
|
||
### 2.5 文件:src/pages/UserManage.tsx
|
||
|
||
| 行号范围 | 改动类型 | 说明 |
|
||
|----------|----------|------|
|
||
| 8 | 删除 | 移除 `ADMIN_DISABLE_AUTH_KEY = 'DISABLE_ADMIN_2024'` |
|
||
| 新增 | 新增 | 从环境变量读取 `ADMIN_AUTH_HASH` 和 `ADMIN_AUTH_SALT` |
|
||
| 29 | 修改 | `authKey` 保持,但比对逻辑改为 SHA-256 |
|
||
| 新增 | 新增 | 增加 `errorMessage`、`confirmDialog` 状态替换 alert/confirm |
|
||
| 103-116 | 修改 | `handleDelete` 改为触发确认对话框状态,移除 `window.confirm` |
|
||
| 118-154 | 修改 | `handleEdit` 中 alert 改为设置 `errorMessage` |
|
||
| 185-324 | 修改 | `handleSubmit` 改为 async,所有 alert 改为 `errorMessage`,授权比对改为 `await sha256(authKey + salt)` |
|
||
| 新增 JSX | 新增 | 表单顶部增加错误提示条;新增轻量确认对话框覆盖层 |
|
||
|
||
---
|
||
|
||
## 3. 关键设计决策
|
||
|
||
| 决策点 | 选择方案 | 备选方案 | 不选备选的原因 |
|
||
|--------|----------|----------|----------------|
|
||
| systemSettings 异步加密的同步接口兼容 | 模块初始化时预解密到内存缓存,get/set 保持同步 | 将 storage.get/set 全局改为 async | 调用点超过 50 处,改动面过大,风险不可控 |
|
||
| AES-GCM 密钥长度处理 | 将环境变量字符串 padEnd 到 32 字节后截断 | 要求用户必须提供 32 字节 | 提升用户体验,减少配置门槛 |
|
||
| 旧数据迁移时机 | init() 中自动检测并静默迁移 | 提供手动迁移脚本 | 纯前端无脚本执行环境,自动迁移对用户透明 |
|
||
| SHA-256 实现选择 | Web Crypto API(已在 storage.ts 提供工具函数) | crypto-js 同步计算 | 统一使用 Web Crypto API,减少额外依赖引用 |
|
||
| alert/confirm 替换策略 | 内联状态提示条 + 轻量确认状态 | 抽取全局 Modal 组件 | 项目中无现成 Modal,本次以最小代价消除明文暴露和原生弹窗 |
|
||
|
||
---
|
||
|
||
## 4. 与现有规范的兼容性检查
|
||
|
||
- [ ] contentEditable 编辑器规范(5.1) — 本次不涉及编辑器
|
||
- [x] 自动保存与草稿机制(5.2) — storage 接口签名不变,ReportEditor 自动保存逻辑无影响
|
||
- [ ] 图片与视频处理(5.3) — 本次不涉及
|
||
- [ ] 智能字段与动态表单(5.4) — 本次不涉及
|
||
- [ ] AI 功能开发规范(5.5) — `getDefaultApiKey()` 返回值不变,调用方无感知
|
||
- [x] 数据迁移与类型安全(5.6) — 旧 XOR 密文自动迁移,使用 `(obj as any).oldField` 安全访问思路已体现在旧格式兼容分支
|
||
- [x] UI/UX 通用规范(5.7) — UserManage.tsx 中原生 alert/confirm 全部替换为自定义状态提示
|
||
|
||
---
|
||
|
||
## 5. 回滚策略
|
||
|
||
若构建失败或运行异常:
|
||
|
||
```bash
|
||
git reset --hard HEAD~1
|
||
```
|
||
|
||
或手动还原:
|
||
1. 恢复 `src/utils/storage.ts` 中的 XOR 逻辑与 `API_KEY_CODES`
|
||
2. 恢复 `src/pages/UserManage.tsx` 中的 `ADMIN_DISABLE_AUTH_KEY`
|
||
3. 恢复 `src/main.tsx` 为同步 render
|
||
4. 还原 `vite.config.ts` 的 define 配置
|
||
|
||
---
|
||
|
||
## 6. 预估工作量
|
||
|
||
| 阶段 | 预估时间 |
|
||
|------|----------|
|
||
| 代码修改 | 25 min |
|
||
| 测试验证 | 15 min |
|
||
| 文档更新 | 10 min |
|
||
|
||
---
|
||
|
||
**状态**:✅ 用户已豁免人工审核确认
|