请求加解密
接口可以在传输层之外再包一层应用层加密:每次请求用随机 AES-256-GCM 密钥加密 JSON,再用服务端 RSA 公钥把这把密钥交给后端。浏览器抓包时看到的是密文;没有加密头的客户端仍按明文访问。
实现位于 server/src/middleware/crypto.ts、server/src/shared/crypto-envelope.ts 与 admin/src/utils/crypto.ts。crypto.enabled 为 false 时中间件不注册。
开关
两边都打开才会加密。任一关闭时,管理端自动退回明文。
| 位置 | 配置 | 作用 |
|---|---|---|
后端 development.yaml / production.yaml | crypto.enabled | 为 true 才注册中间件,并允许下发公钥 |
| 后端同一段 | crypto.privateKey / crypto.publicKey | PKCS8 私钥与 SPKI 公钥(PEM) |
前端 admin/.env* | VITE_API_ENCRYPT | 为 true 才去拉公钥并加密 |
开发配置里 crypto.enabled 默认为 true,生产配置默认为 false。公钥接口是 GET /api/crypto/public-key(ipRateLimit: 60:30,无需登录),返回 { enabled, publicKey }。前端只拉取一次;enabled 为 false、没有公钥或请求失败时,后续请求保持明文。
生成密钥
在 server/ 下执行。脚本只替换 crypto 段的 privateKey / publicKey,不改当前的 enabled。
bun run gen:crypto-keys
bun run gen:crypto-keys production
bun run gen:crypto-keys production --force2
3
已有密钥时不加 --force 会拒绝覆盖,避免旧密钥加密的报文无法再解。私钥只放在服务端 yaml 中,不要提交到仓库。PEM 也可以手工写成 YAML 块,或把换行写成 \n。
生产环境开启前:先生成密钥,再把 crypto.enabled 改为 true,并把前端 VITE_API_ENCRYPT 设为 true 后重新构建。配置结构见 部署 的部署前准备。
协议
算法为 RSA-OAEP(SHA-256)+ AES-256-GCM。IV 为 12 字节,和密文拼在一起再做 Base64。
- 前端用 Web Crypto 生成本次 AES 密钥,公钥加密后放入请求头
X-Encrypt-Key。 - JSON 请求体改为
{ "data": "<Base64(iv + 密文)>" }。GET 等没有 JSON 体的请求只带请求头,响应仍可加密。 - 后端在
onParse里解密,下游 DTO 校验和handle拿到的是明文对象。 - JSON 对象响应加密为
{ "encrypted": true, "data": "<Base64(iv + 密文)>" },并设置响应头x-encrypted: 1。 - 解密失败返回 HTTP 400,文案为「请求解密失败」。
文件上传(FormData 或非 JSON 体)和 blob / arraybuffer / stream 响应不加密。刷新 Token 的独立请求走同一套信封。
明文与日志
没有 X-Encrypt-Key 的请求原样进入后续中间件,第三方调用和本地调试可以继续发 JSON。
响应加密发生在 mapResponse。随后的 onAfterResponse 会把上下文里的 response 还原成加密前的业务对象,操作日志、请求日志和限流记录仍能读取 response.code 等字段。守卫链的位置见 中间件。
