web-admin-api-加解密方案.md 10 KB

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)

前端发给后端的请求体统一为密文壳:

{
  "enc": 1,
  "ts": 1714312345678,
  "nonce": "3f8a1e6d9b4c42f8",
  "edata": "base64url(ciphertext)"
}

其中 edata 解密后的明文结构:

{
  "path": "/web/order/list",
  "method": "POST",
  "data": {
    "page": 1,
    "pageSize": 10,
    "status": "pending"
  }
}

3.2 响应报文

后端返回统一密文壳:

{
  "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,功能与原逻辑一致。
  • 非加密白名单接口行为保持预期。