173 lines
8.6 KiB
Markdown
173 lines
8.6 KiB
Markdown
# 后端收敛与防护设计(最小可落地方案)
|
||
|
||
说明:本文档针对当前仓库中前端直接写 Supabase(存在风险:前端使用 service_role key)问题,提供可执行的最小后台设计、接口与 DB 改造清单,供开发/运维快速落地。
|
||
|
||
---
|
||
|
||
## 目标(Why)
|
||
- 阻断前端持有 `service_role` 导致的越权写库风险。
|
||
- 将关键业务写入(接单/订单状态/资金/库存等)收敛到可信后端,保证原子性、幂等和审计。
|
||
- 在数据库侧加入最后一道防线(约束 + RLS + RPC)。
|
||
|
||
---
|
||
|
||
## 概要执行步骤(What / High level)
|
||
1. 立刻移除前端的 `service_role`,前端只用 `anon`(阻断最大风险)。
|
||
2. 实现最小后台 API(先做配送接单/状态流转 rpc + HTTP 接口)。
|
||
3. 在数据库添加约束、RLS 策略与 RPC(把关键状态流转做成原子函数)。
|
||
4. 前端分阶段切换到新 API 并回归测试。
|
||
5. 补充审计、幂等与监控,逐步迁移其它敏感写入。
|
||
|
||
---
|
||
|
||
## 详细步骤(可直接执行)
|
||
|
||
### A. 紧急措施(立即)
|
||
- 编辑 `ak/config.uts`:注释或删除明文 `SUPA_KEY`(service_role),替为 anon 或从构建环境注入。
|
||
- 确认前端不再把 service_role 打包发布(CI/构建流水线更新)。
|
||
|
||
### B. 最小后端 API(优先交付)
|
||
在 `server/` 下新增 `server/routes/delivery.js`(或 Fastify 插件)。
|
||
|
||
推荐接口(统一返回 `{ ok, code?, message?, data? }`):
|
||
- POST /api/v1/delivery/accept-task
|
||
- body: { task_id, action_id?: string }
|
||
- auth: Bearer user_jwt
|
||
- 后端流程:验证 token → 验证司机权限 → 调用 RPC `rpc_accept_task(uid, task_id, action_id)` → 返回 task 新状态
|
||
- POST /api/v1/delivery/update-status
|
||
- body: { task_id, new_status, action_id, metadata? }
|
||
- 后端流程:验证 → 调用 `rpc_update_delivery_status(...)`(RPC 内部做多表事务)
|
||
- POST /api/v1/notifications/express/create
|
||
- body: notification payload + message_id/action_id(幂等)
|
||
- 后端流程:幂等检查 -> 插入 -> 触发 push(内部队列)
|
||
|
||
后端实现要点:
|
||
- 鉴权:解析并验证前端 Supabase JWT,取 `uid` 作为操作人。
|
||
- 内部调用:后端使用环境变量 `SERVICE_ROLE_KEY` 或直接调用 RPC(推荐)执行受权写入。
|
||
- 返回格式统一并带错误码,便于前端处理与监控。
|
||
|
||
### C. Postgres RPC(示例 SQL)
|
||
下面为 `rpc_accept_task` 草稿,供 DB 管理员在 Dev 环境调整与测试:
|
||
|
||
```sql
|
||
-- rpc_accept_task: driver 领取任务(幂等 + 原子)
|
||
CREATE OR REPLACE FUNCTION public.rpc_accept_task(p_driver_uuid uuid, p_task_uuid uuid, p_action_id text)
|
||
RETURNS jsonb
|
||
LANGUAGE plpgsql
|
||
SECURITY DEFINER
|
||
AS $$
|
||
DECLARE
|
||
v_task RECORD;
|
||
BEGIN
|
||
-- 幂等:插入到去重表,若已存在则返回已处理结果
|
||
INSERT INTO action_dedupe(action_id, created_at)
|
||
VALUES (p_action_id, now())
|
||
ON CONFLICT (action_id) DO NOTHING;
|
||
|
||
-- 执行原子更新:仅当 task 可被领取(status=1 且 driver_id IS NULL)
|
||
WITH u AS (
|
||
UPDATE ml_delivery_tasks
|
||
SET driver_id = p_driver_uuid, status = 2, updated_at = now()
|
||
WHERE id = p_task_uuid AND status = 1 AND driver_id IS NULL
|
||
RETURNING *
|
||
)
|
||
SELECT * INTO v_task FROM u LIMIT 1;
|
||
|
||
IF NOT FOUND THEN
|
||
RETURN jsonb_build_object('ok', false, 'message', 'task not available');
|
||
END IF;
|
||
|
||
-- 写审计
|
||
INSERT INTO audit_logs(actor_id, action, target_table, target_id, payload, created_at)
|
||
VALUES (p_driver_uuid, 'accept_task', 'ml_delivery_tasks', p_task_uuid, row_to_json(v_task), now());
|
||
|
||
RETURN jsonb_build_object('ok', true, 'task', to_jsonb(v_task));
|
||
END;
|
||
$$;
|
||
```
|
||
|
||
注意:
|
||
- 使用 `SECURITY DEFINER` 并确保函数拥有适当权限(仅 server/service_role 可调用)。
|
||
- `action_dedupe` 表需创建并设 unique(action_id)。
|
||
|
||
### D. RLS 与 约束(示例)
|
||
- 开启 RLS,例如对 `user_addresses`:
|
||
```sql
|
||
ALTER TABLE user_addresses ENABLE ROW LEVEL SECURITY;
|
||
CREATE POLICY user_owns_address ON user_addresses
|
||
FOR ALL
|
||
USING (auth.uid() = user_id)
|
||
WITH CHECK (auth.uid() = user_id);
|
||
```
|
||
- 对关键表(`ml_delivery_tasks`、`ml_orders`):
|
||
- 禁止直接由匿名或普通前端更新关键列(例如 driver_id、status);只允许 `rpc` / service 调用通过函数更新。
|
||
- 索引/约束:
|
||
- message_id 唯一:`CREATE UNIQUE INDEX ux_express_notifications_message_id ON express_notifications(message_id);`
|
||
- 补全 NOT NULL / FK / CHECK(枚举字段限制等)。
|
||
|
||
### E. 鉴权实现要点(后端)
|
||
- 验证前端 JWT:解析/校验 Supabase JWT(可使用 Supabase 的用户 API 或直接 JWT 验证)。从 token 获取 `uid` 作为操作人。
|
||
- 后端自身使用环境变量 `SERVICE_ROLE_KEY` 调用 Supabase Admin API 或直接调用 Postgres RPC(建议后者更原子)。
|
||
- 不要在后端把 `service_role` 返回或写进前端文件。
|
||
|
||
### F. 幂等与去重
|
||
- 对所有会被重复调用的外部动作(webhook、client retry)要求 `action_id`/`message_id`,并在 DB 层做 `ON CONFLICT DO NOTHING` 或在 RPC 先检查 `action_dedupe`。
|
||
|
||
### G. 审计与监控
|
||
- 新建 `audit_logs(actor_id, action, target_table, target_id, payload, created_at)` 并在 RPC 中写入。
|
||
- 错误报警(Sentry 或日志轮询),并在关键接口记录 metrics(错误率、latency、冲突数)。
|
||
|
||
---
|
||
|
||
## 迁移与上线路线(分阶段)
|
||
1. Dev:实现 RPC + API,更新 dev 配置把前端用 anon,完成单元测试与集成测试。
|
||
2. Stage:灰度发布后端 API,部分司机/少量流量切换到 API。监控 48h。
|
||
3. Prod:全面切换,删除前端中的直写调用(或通过 feature-flag 关闭)。
|
||
4. 回滚:保留旧直写代码并用 feature flag 随时回退;若回退需同时短期恢复前端原 key(仅极端应急,注意风险并在短时间内移除)。
|
||
|
||
---
|
||
|
||
## 最小交付清单(可追踪任务)
|
||
- [ ] 注释/移除前端 `service_role`:`ak/config.uts`(紧急)
|
||
- [ ] 在 `server/` 新增 `POST /api/v1/delivery/accept-task`(鉴权 + 调用 RPC)
|
||
- [ ] 在 DB 创建 `rpc_accept_task`、`action_dedupe`、`audit_logs` 表
|
||
- [ ] 对 `user_addresses`、`ml_delivery_tasks`、`ml_orders` 等启用 RLS 策略(逐表)
|
||
- [ ] 前端将接单/确认调用迁移到新 API(feature-flag)并回归测试
|
||
- [ ] 部署后监控并逐步扩大灰度
|
||
|
||
---
|
||
|
||
## 风险与注意事项
|
||
- 风险:短期内若不移除 `service_role`,即便实现后台 API,也无法阻止攻击者通过已有 key 直接写库。优先级最高。
|
||
- 注意隐私/合规:审计日志要避免写入敏感明文(例如完整支付凭证)或加密存储。
|
||
- DB 权限:确保 RPC 使用受限角色执行并限制 RPC 所能做的动作。
|
||
|
||
---
|
||
|
||
## 推荐下一步(我可以马上帮你做)
|
||
- 选项 1:生成并提交 `ak/config.uts` 的 patch,将 `SUPA_KEY` 替换为示例 anon(阻断风险)。
|
||
- 选项 2:生成 `rpc_accept_task` 的完整 SQL + `server/routes/delivery.js` 的 Node 实现样例(包含鉴权中间件与调用示例)。
|
||
- 选项 3:列出仓库内所有前端写入点并按“必须走后端 / 可保留直写”分级清单(便于逐步迁移)。
|
||
|
||
请回复 `1`、`2` 或 `3` 选项,或告诉我需要调整的文档内容/格式。
|
||
|
||
---
|
||
## 附录:前端不得直接连接数据库(简明实施指南)
|
||
|
||
结论:所有需要提升权限、跨表原子性、审计或幂等保证的写入必须走可信后端;仅在严格受限(RLS + 约束)且只影响用户自身资源时,前端可用 anon key 直写。
|
||
|
||
- 何时必须走后端:更新 `driver_id`、订单状态、资金/结算、库存变更、跨表事务、需要审计或幂等的操作、需使用 `service_role` 权限的写入。
|
||
- 何时可允许前端直写:仅用户自身数据(例如 `user_addresses`)且已启用 RLS 与完整约束,且不涉及跨表或审计要求。
|
||
|
||
最小后端职责(示例):
|
||
- 验证前端 JWT(取 `uid`);做权限校验与幂等检查(`action_id`)。
|
||
- 调用 Postgres RPC(`SECURITY DEFINER`)或使用 `SERVICE_ROLE_KEY` 完成受权写入。
|
||
- 写入 `audit_logs` 并返回统一错误格式 `{ ok, code?, message?, data? }`。
|
||
|
||
紧急建议(复述以便执行):
|
||
- 立即移除或注释前端的 `service_role`(已在工作区修改 `ak/config.uts`)。
|
||
- 在 CI 中阻止带有 `service_role` 的 key 进入前端配置或打包产物。
|
||
- 为关键流实现 RPC(如 `rpc_accept_task`)并暴露最小后端 API(如 `/api/v1/delivery/accept-task`),逐步将前端写入迁移到后端。
|
||
|
||
需要我把这个附录再整理为单独文件或把 `rpc_accept_task` SQL 与 `server/routes/delivery.js` 示例直接追加到本文件吗?
|