# Web 端 API 入参与出参加解密方案 ## 1. 目标与范围 - 目标:`web-admin` 调用 `backend` 时,请求入参与响应出参均为密文,降低接口明文暴露风险。 - 范围:Web 端全部业务 API(含认证相关接口),并将原 GET 查询接口统一迁移为 POST。 - 约束:尽量保持现有业务层接口处理逻辑不变,优先通过请求/响应拦截与中间件统一处理。 ## 2. 方案总览 本方案采用“每次请求动态派生密钥 + 请求/响应统一密文壳”的方式。 - 密钥派生公式(双方一致): - `derivedKey = SHA256(fixed + ":" + userId + ":" + timestamp + ":" + nonce)` - `timestamp`:请求时间戳(毫秒) - `nonce`:每次请求随机生成(不可固定) - 对称加密算法:AES-GCM(推荐) - 编码:Base64URL(避免 URL 与 JSON 传输字符冲突) 说明: - `nonce` 必须随机且一次性使用,用于防重放与降低密钥重用风险。 - `fixed` 不能作为硬编码明文长期暴露在前端;建议改为登录后下发的短时会话密钥(仅内存保存,随登录态失效)。 ## 3. 请求与响应报文结构 ### 3.1 请求报文(统一 POST) 前端发给后端的请求体统一为密文壳: ```json { "enc": 1, "ts": 1714312345678, "nonce": "3f8a1e6d9b4c42f8", "edata": "base64url(ciphertext)" } ``` 其中 `edata` 解密后的明文结构: ```json { "path": "/web/order/list", "method": "POST", "data": { "page": 1, "pageSize": 10, "status": "pending" } } ``` ### 3.2 响应报文 后端返回统一密文壳: ```json { "code": 200, "encrypted": true, "ts": 1714312345890, "nonce": "b1c27f8d0e5a43aa", "edata": "base64url(ciphertext)" } ``` 其中 `edata` 解密后仍是原业务响应结构(例如 `{ code, message, data, timestamp }` 或其 `data` 部分,按现有封装约定保持一致)。 ## 4. 防重放与时效校验 后端在解密前先做安全校验: - 时间窗校验:`abs(serverNow - ts) <= 300000ms`(建议 5 分钟) - nonce 一次性校验:同一 `userId + nonce` 在窗口期内只能使用一次 - 存储方式:Redis 记录 `enc:nonce:{userId}:{nonce}`,TTL=300s 任一校验失败,返回统一错误: - `400`:密文格式非法 - `401`:未授权或 token 无效 - `409`:重放请求(nonce 重复) - `408/400`:时间戳过期或超窗 ## 5. GET 改 POST 迁移规范 为统一加密流程,Web 端原 GET 查询接口全部迁移为 POST: - 前端: - 原 `api.get({ url, params })` 改为 `api.post({ url, data })` - 查询参数放入请求体 `data` - 后端: - 路由由 `@router.get(...)` 改为 `@router.post(...)` - 参数读取由 query 改为 body - 兼容策略(推荐): - 过渡期同时支持 GET + POST - 前端全量切 POST 后,再移除 GET ## 6. 前端改造点(web-admin) 核心改造集中在 `src/utils/http/index.ts`: - 请求拦截器 - 读取当前登录用户 `userId` - 生成 `ts`、随机 `nonce` - 按公式派生 `derivedKey` - 对请求体加密并封装为 `{ enc, ts, nonce, edata }` - 响应拦截器 - 识别 `encrypted=true` - 用响应 `ts + nonce` 派生 key 并解密 `edata` - 还原为业务层当前期望的数据结构 - 白名单 - 登录前公钥接口、登录接口本身可按迁移节奏保留明文/专用加密逻辑 ## 7. 后端改造点(backend) 建议新增统一加解密模块与中间件: - 新增 `app/core/api_crypto.py` - `derive_key(fixed_secret, user_id, ts, nonce)`:按 `SHA256(fixed:userId:ts:nonce)` 派生 32 字节 key - `encrypt_json(...)` / `decrypt_json(...)`:AES-GCM + Base64URL - `is_ts_in_window(...)`:时间窗校验 - 新增 `app/core/api_crypto_middleware.py` - 解析并校验请求壳 `{ enc, ts, nonce, edata }` - `nonce` 防重放(优先 Redis;不可用时本地内存降级) - 响应统一包裹为 `{ code, encrypted, ts, nonce, edata }` - 兼容 `X-HTTP-Method-Override: GET`(用于“GET 改 POST 传输”过渡期) - 新增/扩展中间件(在路由前) - 解析 `enc` 请求壳并解密回填到请求对象 - 完成 `nonce` 防重放校验(Redis) - 在响应阶段统一加密输出壳 - 白名单路由 - 登录、公钥、健康检查、文件下载等按需放行或定制 ## 8. 配置项建议 后端配置(示例): - `API_ENCRYPT_ENABLED=true` - `API_ENCRYPT_FIXED_SECRET=...` - `API_ENCRYPT_FAIL_OPEN=true` - `API_ENCRYPT_TS_SKEW_SEC=300` - `API_ENCRYPT_NONCE_TTL_SEC=300` - `API_ENCRYPT_EXCLUDE_PATHS=/,/health,/docs,/openapi.json,/api/web/auth/login-rsa-public-key,/api/web/auth/login,/api/web/admin/attachment/upload,/api/web/admin/products/upload-image,/api/web/admin/categories/upload-image,/api/web/admin/users/upload-avatar,/api/invoice/download` 前端配置(示例): - `VITE_API_ENCRYPT_ENABLED=true` - `VITE_API_ENCRYPT_FAIL_OPEN=true` - `VITE_API_ENCRYPT_FIXED_SECRET=与后端一致` - `VITE_API_ENCRYPT_EXCLUDE_PATHS=/web/auth/login-rsa-public-key,/web/auth/login` ## 9. 实施步骤(建议) 1. 后端先落地“可选加密能力”(开关关闭) 2. 前端接入请求/响应加解密拦截器(仅灰度接口) 3. 开启灰度接口加密,验证登录态、分页、导出、上传等场景 4. 批量迁移 GET->POST 5. 全量开启加密开关 6. 清理兼容逻辑(如旧 GET、旧明文路径) ### 9.1 实施进度(逐模块联调清单) 判定规则(当前实现): - **默认加密**:`/web/*` 且已登录(可取到 `userId`),且不在前端排除列表,且非 `FormData` - **默认白名单**:登录前接口、上传(`multipart/form-data`)、下载(二进制/流)等 - **GET 改 POST 传输**:前端会把 GET 自动改为 POST,并携带 `X-HTTP-Method-Override: GET`(后端中间件可还原为 GET 语义) #### auth(`web-admin/src/api/auth.ts`) | 接口 | 是否加密 | 是否白名单 | 特殊处理 | |---|---:|---:|---| | `/web/auth/login-rsa-public-key` | 否 | 是 | 登录前获取 RSA 公钥(现有逻辑) | | `/web/auth/login` | 否 | 是 | 登录本身沿用 `encryptedLogin`(RSA) | | `/web/auth/current-user` | 是 | 否 | GET->POST 传输兼容 | | `/web/auth/refresh-token` | 是 | 否 | POST;注意 token 刷新稳定性 | | `/web/auth/logout` | 是 | 否 | POST | #### business(`web-admin/src/api/business.ts`) | 接口(前缀/代表) | 是否加密 | 是否白名单 | 特殊处理 | |---|---:|---:|---| | `/web/admin/orders*` | 是 | 否 | 多处 GET->POST 传输兼容 | | `/web/delivery-tasks*` | 是 | 否 | GET->POST 传输兼容 | | `/web/admin/delivery-warning/*` | 是 | 否 | GET->POST 传输兼容 | | `/web/admin/inquiries*` | 是 | 否 | GET->POST 传输兼容 | | `/web/admin/quotations*` / `/web/admin/quotation-compare` | 是 | 否 | GET->POST 传输兼容 | | `/web/admin/price-lists*` / `/web/admin/price-versions*` | 是 | 否 | GET->POST 传输兼容 | | `/web/admin/*settlements*` / `/web/admin/settlements*` | 是 | 否 | GET->POST 传输兼容 | | `/web/admin/refunds*` | 是 | 否 | GET->POST 传输兼容 | | `/web/admin/statistics/*` | 是 | 否 | GET->POST 传输兼容 | | `/web/admin/api-request-logs` | 是 | 否 | GET->POST 传输兼容 | | `/web/admin/invoice/*`(列表/详情/创建/处理) | 是 | 否 | GET->POST 传输兼容 | | `/api/invoice/download/:id` | 否 | 是 | **下载流**:不走 JSON 密文壳(需白名单) | | `/web/admin/products/upload-image` | 否 | 是 | **上传**:`FormData`(自动绕过加密) | | `/web/admin/attachment/upload` | 否 | 是 | **上传**:`FormData`(自动绕过加密) | | `/web/admin/system-config/settings` | 是 | 否 | GET->POST 传输兼容 | #### admin(`web-admin/src/api/admin.ts`) | 接口(前缀/代表) | 是否加密 | 是否白名单 | 特殊处理 | |---|---:|---:|---| | `/web/admin/users*` | 是 | 否 | GET->POST 传输兼容 | | `/web/admin/users/upload-avatar` | 否 | 是 | **上传**:`FormData`(自动绕过加密) | | `/web/admin/regions*` | 是 | 否 | GET->POST 传输兼容 | | `/web/admin/categories*` | 是 | 否 | GET->POST 传输兼容 | | `/web/admin/categories/upload-image` | 否 | 是 | **上传**:`FormData`(自动绕过加密) | | `/web/admin/units*` | 是 | 否 | GET->POST 传输兼容 | | `/web/admin/products*`(除上传) | 是 | 否 | GET->POST 传输兼容 | | `/web/admin/product-banners*` | 是 | 否 | GET->POST 传输兼容 | | `/web/admin/product-detail-media*` | 是 | 否 | GET->POST 传输兼容 | | `/web/admin/product-parameters*` | 是 | 否 | GET->POST 传输兼容 | | `/web/admin/product-parameter-values*` | 是 | 否 | GET->POST 传输兼容 | | `/web/admin/reviews*` | 是 | 否 | GET->POST 传输兼容 | #### system / system-manage / chinaRegion(`web-admin/src/api/system.ts`、`system-manage.ts`、`chinaRegion.ts`) | 接口 | 是否加密 | 是否白名单 | 特殊处理 | |---|---:|---:|---| | `/web/system/upgrade-logs*` | 是 | 否 | GET->POST 传输兼容 | | `/web/system/comments*` | 是 | 否 | GET->POST 传输兼容 | | `/web/system/china-map` | 是 | 否 | GET->POST 传输兼容 | | `/web/system/config/:key` | 是 | 否 | GET->POST 传输兼容 | | `/web/system/version` | 是 | 否 | GET->POST 传输兼容 | | `/web/menu/list` | 是 | 否 | GET->POST 传输兼容 | | `/web/admin/roles` | 是 | 否 | GET->POST 传输兼容 | | `/web/admin/china-region-tree` | 是 | 否 | GET->POST 传输兼容 | ## 10. 风险与边界 - 若 `fixed` 泄露且前端可直接得到,则整体安全收益下降。 - 文件上传(`multipart/form-data`)通常不走同一 JSON 密文壳,需单独策略(例如仅加签)。 - 下载流(导出 Excel、二进制文件)不建议强制套 JSON 密文壳,需白名单放行或使用独立下载签名机制。 - API 请求日志中需继续对 `edata`、token、密码等字段打码,避免“日志明文化”。 ## 11. 验收标准 - Web 端抓包中,请求业务参数与响应业务数据均不可见明文。 - 重放同一请求(相同 `ts + nonce + edata`)会被后端拒绝。 - 时间窗外请求被拒绝。 - 迁移后的查询接口全部使用 POST,功能与原逻辑一致。 - 非加密白名单接口行为保持预期。