web-admin 调用 backend 时,请求入参与响应出参均为密文,降低接口明文暴露风险。本方案采用“每次请求动态派生密钥 + 请求/响应统一密文壳”的方式。
derivedKey = SHA256(fixed + ":" + userId + ":" + timestamp + ":" + nonce)timestamp:请求时间戳(毫秒)nonce:每次请求随机生成(不可固定)说明:
nonce 必须随机且一次性使用,用于防重放与降低密钥重用风险。fixed 不能作为硬编码明文长期暴露在前端;建议改为登录后下发的短时会话密钥(仅内存保存,随登录态失效)。前端发给后端的请求体统一为密文壳:
{
"enc": 1,
"ts": 1714312345678,
"nonce": "3f8a1e6d9b4c42f8",
"edata": "base64url(ciphertext)"
}
其中 edata 解密后的明文结构:
{
"path": "/web/order/list",
"method": "POST",
"data": {
"page": 1,
"pageSize": 10,
"status": "pending"
}
}
后端返回统一密文壳:
{
"code": 200,
"encrypted": true,
"ts": 1714312345890,
"nonce": "b1c27f8d0e5a43aa",
"edata": "base64url(ciphertext)"
}
其中 edata 解密后仍是原业务响应结构(例如 { code, message, data, timestamp } 或其 data 部分,按现有封装约定保持一致)。
后端在解密前先做安全校验:
abs(serverNow - ts) <= 300000ms(建议 5 分钟)userId + nonce 在窗口期内只能使用一次enc:nonce:{userId}:{nonce},TTL=300s任一校验失败,返回统一错误:
400:密文格式非法401:未授权或 token 无效409:重放请求(nonce 重复)408/400:时间戳过期或超窗为统一加密流程,Web 端原 GET 查询接口全部迁移为 POST:
api.get({ url, params }) 改为 api.post({ url, data })data@router.get(...) 改为 @router.post(...)核心改造集中在 src/utils/http/index.ts:
userIdts、随机 noncederivedKey{ enc, ts, nonce, edata }encrypted=truets + nonce 派生 key 并解密 edata建议新增统一加解密模块与中间件:
app/core/api_crypto.py
derive_key(fixed_secret, user_id, ts, nonce):按 SHA256(fixed:userId:ts:nonce) 派生 32 字节 keyencrypt_json(...) / decrypt_json(...):AES-GCM + Base64URLis_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)后端配置(示例):
API_ENCRYPT_ENABLED=trueAPI_ENCRYPT_FIXED_SECRET=...API_ENCRYPT_FAIL_OPEN=trueAPI_ENCRYPT_TS_SKEW_SEC=300API_ENCRYPT_NONCE_TTL_SEC=300API_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=trueVITE_API_ENCRYPT_FAIL_OPEN=trueVITE_API_ENCRYPT_FIXED_SECRET=与后端一致VITE_API_ENCRYPT_EXCLUDE_PATHS=/web/auth/login-rsa-public-key,/web/auth/login判定规则(当前实现):
/web/* 且已登录(可取到 userId),且不在前端排除列表,且非 FormDatamultipart/form-data)、下载(二进制/流)等X-HTTP-Method-Override: GET(后端中间件可还原为 GET 语义)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 |
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 传输兼容 |
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 传输兼容 |
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 传输兼容 |
fixed 泄露且前端可直接得到,则整体安全收益下降。multipart/form-data)通常不走同一 JSON 密文壳,需单独策略(例如仅加签)。edata、token、密码等字段打码,避免“日志明文化”。ts + nonce + edata)会被后端拒绝。