refactor: 迁移本地 Supabase 到云端,使用 JWKS 进行 JWT 验证

- 新增 JwtVerifier 支持 RS256 + JWKS 验证
- 简化 docker-compose,删除本地 Supabase 服务(kong/auth/storage等)
- 删除冗余的 Supabase 配置文件(volumes目录)
- 适配测试用例以支持新配置方式
- 更新运行时文档和迁移计划
This commit is contained in:
qzl
2026-03-09 18:03:04 +08:00
parent 3ac09475ad
commit 6fe2e7b6c3
24 changed files with 825 additions and 1403 deletions
@@ -0,0 +1,303 @@
# Cloud Supabase Env Cleanup & JWKS Migration Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** 切换到云 Supabase 后,移除本地自托管 Supabase 基础设施变量与编排,保留 Redis + DB + init-job,并将后端 JWT 验签从 `JWT_SECRET` 改为 JWKS 公钥验签。
**Architecture:** 后端配置收敛到“业务运行所需最小集合”(Supabase URL/anon/service role + DB + Redis)。认证链路采用 JWKS 拉取公钥并按 `kid` 验签,替代共享密钥 HS256。Docker 编排只保留业务依赖(redis、db、init-job),不再编排本地 Supabase 全家桶。
**Tech Stack:** FastAPI, Pydantic Settings, PyJWT (PyJWKClient), Docker Compose, pytest
---
### Task 1: 固化云模式配置契约(先测后改)
**Files:**
- Modify: `backend/tests/unit/test_settings_supabase_env.py`
- Modify: `.env.example`
**Step 1: 写失败测试,定义新 Supabase 配置契约**
```python
def test_social_prefixed_supabase_env_populates_settings(monkeypatch: MonkeyPatch) -> None:
monkeypatch.setenv("SOCIAL_SUPABASE__PUBLIC_URL", "https://project.example.supabase.co")
monkeypatch.setenv("SOCIAL_SUPABASE__ANON_KEY", "anon-key")
monkeypatch.setenv("SOCIAL_SUPABASE__SERVICE_ROLE_KEY", "service-key")
monkeypatch.setenv("SOCIAL_SUPABASE__JWT_AUDIENCE", "authenticated")
settings = Settings()
assert settings.supabase.public_url == "https://project.example.supabase.co"
assert settings.supabase.jwt_issuer == "https://project.example.supabase.co/auth/v1"
assert settings.supabase.jwks_url.endswith("/auth/v1/.well-known/jwks.json")
```
**Step 2: 运行测试确认失败**
Run: `uv run pytest backend/tests/unit/test_settings_supabase_env.py -v`
Expected: FAIL`public_url/jwks_url` 字段不存在或断言失败)
**Step 3: 最小改动让测试通过(仅 settings 相关,逻辑改动在后续任务)**
更新 `.env.example` 为云模式最小变量草案(先占位,后续任务会补最终文案):
- `SOCIAL_SUPABASE__PUBLIC_URL=`
- `SOCIAL_SUPABASE__ANON_KEY=`
- `SOCIAL_SUPABASE__SERVICE_ROLE_KEY=`
- `SOCIAL_SUPABASE__JWT_AUDIENCE=authenticated`
- `SOCIAL_SUPABASE__JWT_ISSUER=`(可选,默认由 PUBLIC_URL 推导)
- `SOCIAL_SUPABASE__JWKS_URL=`(可选,默认由 PUBLIC_URL 推导)
**Step 4: 运行测试确认通过**
Run: `uv run pytest backend/tests/unit/test_settings_supabase_env.py -v`
Expected: PASS
**Step 5: Commit**
```bash
git add backend/tests/unit/test_settings_supabase_env.py .env.example
git commit -m "test: define cloud supabase settings contract"
```
### Task 2: 重构 SupabaseSettings(移除 JWT_SECRET 依赖)
**Files:**
- Modify: `backend/src/core/config/settings.py`
- Modify: `backend/tests/unit/test_settings_supabase_env.py`
**Step 1: 写失败测试,约束默认推导行为**
```python
assert settings.supabase.jwt_issuer == "https://project.example.supabase.co/auth/v1"
assert settings.supabase.jwks_url == "https://project.example.supabase.co/auth/v1/.well-known/jwks.json"
assert "jwt_secret" not in settings.model_dump()["supabase"]
```
**Step 2: 运行测试确认失败**
Run: `uv run pytest backend/tests/unit/test_settings_supabase_env.py -v`
Expected: FAIL
**Step 3: 实现最小配置重构**
`SupabaseSettings` 中改为:
- 必填:`public_url`, `anon_key`, `service_role_key`
- 可选:`site_url`, `additional_redirect_urls`
- 新增:`jwt_audience`(默认 `authenticated`)、`jwt_issuer`(默认 `${public_url}/auth/v1`)、`jwks_url`(默认 `${jwt_issuer}/.well-known/jwks.json`
- 删除:`jwt_secret`, `public_scheme`, `public_host`, `kong_http_port`, `kong_https_port`
**Step 4: 运行测试确认通过**
Run: `uv run pytest backend/tests/unit/test_settings_supabase_env.py -v`
Expected: PASS
**Step 5: Commit**
```bash
git add backend/src/core/config/settings.py backend/tests/unit/test_settings_supabase_env.py
git commit -m "refactor: migrate supabase config to cloud jwks fields"
```
### Task 3: 引入 JWKS 验签组件并接入认证依赖
**Files:**
- Create: `backend/src/core/auth/jwt_verifier.py`
- Modify: `backend/src/v1/users/dependencies.py`
- Create: `backend/tests/unit/core/auth/test_jwt_verifier.py`
**Step 1: 先写失败测试(JWT 验签核心行为)**
```python
def test_verify_token_with_jwks_success(...):
claims = verifier.verify(token)
assert claims["sub"] == str(user_id)
def test_verify_token_rejects_invalid_issuer(...):
with pytest.raises(TokenValidationError):
verifier.verify(token_with_wrong_iss)
```
**Step 2: 运行测试确认失败**
Run: `uv run pytest backend/tests/unit/core/auth/test_jwt_verifier.py -v`
Expected: FAIL(模块/类不存在)
**Step 3: 实现最小 JWKS 验签逻辑**
```python
class JwtVerifier:
def __init__(self, jwks_url: str, issuer: str, audience: str) -> None: ...
def verify(self, token: str) -> dict[str, Any]:
key = self._jwks_client.get_signing_key_from_jwt(token)
return jwt.decode(
token,
key.key,
algorithms=["RS256", "ES256"],
audience=self._audience,
issuer=self._issuer,
options={"require": ["sub", "aud", "iss", "exp"]},
)
```
`get_current_user` 中替换原 `jwt_secret + HS256` 验签,统一映射为现有 401/503 语义。
**Step 4: 运行测试确认通过**
Run: `uv run pytest backend/tests/unit/core/auth/test_jwt_verifier.py -v`
Expected: PASS
**Step 5: Commit**
```bash
git add backend/src/core/auth/jwt_verifier.py backend/src/v1/users/dependencies.py backend/tests/unit/core/auth/test_jwt_verifier.py
git commit -m "feat: validate access tokens via supabase jwks"
```
### Task 4: 回归认证路径与 live 测试兼容
**Files:**
- Modify: `backend/tests/integration/v1/agent/test_sse_flow_live.py`
- Modify: `backend/tests/integration/test_auth_routes.py`(如需)
**Step 1: 写失败测试/调整 live 测试生成 token 方式**
将 live 测试从“本地签发 HS256 token”改为“通过真实登录拿 access token”或“无测试账号时 skip”。
```python
if not os.getenv("AGENT_LIVE_EMAIL") or not os.getenv("AGENT_LIVE_PASSWORD"):
pytest.skip("missing live supabase credentials")
```
**Step 2: 运行相关测试确认失败(或旧逻辑不适配)**
Run: `uv run pytest backend/tests/integration/v1/agent/test_sse_flow_live.py -m live -v`
Expected: 在旧代码下不可用/依赖 jwt_secret
**Step 3: 完成最小实现改造**
- 移除 `config.supabase.jwt_secret` 的测试依赖。
- 保持 `@pytest.mark.live` 行为不变,避免影响常规 CI。
**Step 4: 运行测试确认通过(或受控 skip)**
Run: `uv run pytest backend/tests/integration/v1/agent/test_sse_flow_live.py -m live -v`
Expected: PASS 或可解释的 SKIP(凭证缺失)
**Step 5: Commit**
```bash
git add backend/tests/integration/v1/agent/test_sse_flow_live.py backend/tests/integration/test_auth_routes.py
git commit -m "test: align live auth flow with cloud supabase tokens"
```
### Task 5: 裁剪 Docker Compose(移除本地 Supabase,保留 Redis/DB/init-job
**Files:**
- Modify: `infra/docker/docker-compose.yml`
**Step 1: 写失败验证(compose 结构断言)**
添加一个轻量脚本化检查(可在本任务临时执行,不必入库):
```bash
docker compose --env-file .env -f infra/docker/docker-compose.yml config
```
在改造前记录当前包含的 Supabase 服务(`studio/kong/auth/rest/...`)作为对照。
**Step 2: 执行检查确认当前状态(基线)**
Run: `docker compose --env-file .env -f infra/docker/docker-compose.yml config`
Expected: 输出包含 Supabase 全家桶服务
**Step 3: 最小实现裁剪**
- 删除服务:`studio/kong/mail-templates/auth/rest/realtime/storage/imgproxy/meta/functions/analytics/vector/supavisor`
- 保留服务:`redis`, `db`, `init-job`
- `init-job` 环境变量移除:`SOCIAL_SUPABASE__ANON_KEY`, `SOCIAL_SUPABASE__SERVICE_ROLE_KEY`, `SOCIAL_SUPABASE__JWT_SECRET`
- `db` 服务切换为业务最小化所需配置(仅数据库启动与健康检查必需)
**Step 4: 运行 compose 校验**
Run: `docker compose --env-file .env -f infra/docker/docker-compose.yml config`
Expected: PASS,且仅保留 redis/db/init-job
**Step 5: Commit**
```bash
git add infra/docker/docker-compose.yml
git commit -m "refactor: remove local supabase stack from compose"
```
### Task 6: 清理环境模板与运行文档
**Files:**
- Modify: `.env.example`
- Modify: `docs/runtime/runtime-runbook.md`
- Modify: `infra/scripts/dev-migrate.sh`
**Step 1: 先写文档/模板检查点(人工可核验)**
定义必须满足:
- `.env.example` 不再包含本地 Supabase 基础设施变量(logflare/pooler/studio/kong/jwt_secret 等)
- 保留并标注后端必需项:`PUBLIC_URL`, `ANON_KEY`, `SERVICE_ROLE_KEY`
- runbook 的健康检查改为 Redis/DB/Web,而非 Kong
**Step 2: 运行基线检查(改造前)**
Run: `uv run pytest backend/tests/unit/test_settings_supabase_env.py -v`
Expected: 作为环境模板改造后的回归基线
**Step 3: 最小实现文档更新**
- `docs/runtime/runtime-runbook.md`:把“启动基础设施”描述改为 `redis + db`
- `infra/scripts/dev-migrate.sh`:将提示从“Requires Supabase services”改为“Requires db/redis services”。
- `.env.example`:按云模式分组,明确前端/后端变量边界。
**Step 4: 运行检查确认通过**
Run: `docker compose --env-file .env -f infra/docker/docker-compose.yml config`
Expected: PASS
**Step 5: Commit**
```bash
git add .env.example docs/runtime/runtime-runbook.md infra/scripts/dev-migrate.sh
git commit -m "docs: update runtime guide for cloud supabase mode"
```
### Task 7: 全量验证与发布前检查
**Files:**
- Modify: `docs/runtime/runtime-runbook.md`(记录验证命令与结果)
**Step 1: 运行静态检查**
Run: `uv run ruff check backend/src backend/tests`
Expected: PASS
**Step 2: 运行类型检查**
Run: `uv run basedpyright`
Expected: PASS
**Step 3: 运行测试(按影响面)**
Run: `uv run pytest backend/tests/unit/test_settings_supabase_env.py backend/tests/unit/core/auth/test_jwt_verifier.py -v`
Expected: PASS
Run: `uv run pytest backend/tests/integration/test_users_routes.py backend/tests/integration/test_auth_routes.py -v`
Expected: PASS
**Step 4: 运行运行时门禁验证**
Run: `docker compose --env-file .env -f infra/docker/docker-compose.yml up -d redis db && docker compose --env-file .env -f infra/docker/docker-compose.yml run --rm --build init-job uv run python -m core.runtime.cli bootstrap`
Expected: PASS(迁移 + init-data 成功)
**Step 5: Commit**
```bash
git add docs/runtime/runtime-runbook.md
git commit -m "chore: record cloud supabase migration verification"
```
+19 -21
View File
@@ -28,10 +28,10 @@
### Step 1: 启动基础设施
```bash
docker compose --env-file .env -f infra/docker/docker-compose.yml up -d
docker compose --env-file .env -f infra/docker/docker-compose.yml up -d redis
```
通过标准:`docker compose ... ps` 中 redis/supabase 相关容器为 `running`
通过标准:`docker compose ... ps``redis` 容器为 `running`/`healthy`
### Step 2: 执行迁移与初始化
@@ -114,8 +114,11 @@ set +a
WEB_BASE_URL="http://127.0.0.1:${SOCIAL_WEB__PORT:-5775}"
# 基础健康
curl -fsS http://127.0.0.1:${SOCIAL_SUPABASE__KONG_HTTP_PORT:-8000}/health
# 基础健康redis/web;数据库使用云 Supabase Postgres
docker compose --env-file .env -f infra/docker/docker-compose.yml exec -T redis \
sh -lc 'if [ -n "${REDIS_PASSWORD:-}" ]; then redis-cli -a "${REDIS_PASSWORD}" ping; else redis-cli ping; fi'
curl -fsS "${WEB_BASE_URL}/health"
# compose 状态
docker compose --env-file .env -f infra/docker/docker-compose.yml ps
@@ -126,7 +129,7 @@ curl -sS -X POST "${WEB_BASE_URL}/api/v1/auth/sessions" \
-d '{"email":"demo@example.com","password":"secret123"}'
```
通过标准:health 返回 2xx关键容器 `running`,核心接口返回预期业务状态码。
通过标准:redis 健康检查成功,web `/health` 返回 2xx,容器 `running`,核心接口返回预期业务状态码。
### L2 可选(Auth/Profile 业务回归)
@@ -193,31 +196,25 @@ docker compose --env-file .env -f infra/docker/docker-compose.yml exec -T redis
- 定位:核对 `.env` 中 Supabase JWT 配置与签发方设置。
- 修复:修正配置后重启 web 进程并执行 L1/L2 验证。
### 4) Auth 邮件模板未生效 / 注册返回超时但邮件已发送
### 4) 基础设施容器异常(db/redis
- 症状:
- 收到默认英文模板(非 `infra/mail-templates`)。
- `signup/start` 偶发 500 或超时,但邮箱仍收到验证码邮件。
- 根因:容器配置漂移(旧容器未按最新 compose/.env 重建),导致:
- `supabase-auth` 缺少 `GOTRUE_MAILER_TEMPLATES_*` 环境变量。
- `supabase-mail-templates` 仍挂载旧路径。
- 症状:web 启动失败、迁移失败、任务队列连接报错。
- 定位:
```bash
docker inspect supabase-auth --format '{{ range .Config.Env }}{{ println . }}{{ end }}' | grep GOTRUE_MAILER_TEMPLATES
docker inspect supabase-mail-templates --format '{{ range .Mounts }}{{ .Source }} -> {{ .Destination }}{{ println }}{{ end }}'
docker compose --env-file .env -f infra/docker/docker-compose.yml ps
docker compose --env-file .env -f infra/docker/docker-compose.yml logs db --tail=100
docker compose --env-file .env -f infra/docker/docker-compose.yml logs redis --tail=100
```
- 修复:强制重建 auth 和 mail-templates(不改其他服务):
- 修复:按依赖顺序重建基础设施后重新 bootstrap。
```bash
docker compose --env-file .env -f infra/docker/docker-compose.yml up -d --force-recreate --no-deps mail-templates auth
docker compose --env-file .env -f infra/docker/docker-compose.yml up -d --force-recreate redis
bash infra/scripts/dev-migrate.sh bootstrap
```
- 复核标准:
- `docker inspect supabase-auth` 能看到 `GOTRUE_MAILER_TEMPLATES_CONFIRMATION/RECOVERY`
- `supabase-mail-templates` 挂载源为 `infra/mail-templates`
- `POST /api/v1/auth/verifications` 返回 `202` 且耗时恢复正常。
- 复核标准:`redis` 健康检查通过,L1 核心接口 smoke 无 5xx。
---
@@ -248,7 +245,7 @@ docker compose --env-file .env -f infra/docker/docker-compose.yml up -d --force-
|------|------|
| 2026-02-24 | 创建运行时手册,删除 legacy 脚本,统一使用 gunicorn |
| 2026-02-24 | 清理配置:合并 AppSettings 到 WebSettings,删除 Worker 旧配置 (enabled_queues/queues),统一使用 SOCIAL_WEB__GUNICORN__* 命名 |
| 2026-02-24 | 开发阶段 compose 暂不编排 web/worker,仅保留 redis/supabase 与 init-job |
| 2026-02-24 | 开发阶段 compose 暂不编排 web/worker,仅保留 redis/db 与 init-job |
| 2026-02-24 | 新增 dev-app-up 脚本:手动基础设施后,一键 bootstrap + tmux 拉起 web/worker |
| 2026-02-25 | 补充迁移防遗漏规则:容器迁移命令统一追加 --build;开发调试优先使用本地 CLI 一次性迁移脚本 |
| 2026-02-25 | Auth 注册切换为 OTP 三段式:signup/start、signup/verify、signup/resend;邮件模板改为纯验证码展示 |
@@ -263,3 +260,4 @@ docker compose --env-file .env -f infra/docker/docker-compose.yml up -d --force-
| 2026-03-02 | 修正 bootstrap 命令:init-job 需要使用 `uv run python -m core.runtime.cli bootstrap` |
| 2026-03-05 | 新增 Agent Runtime run/resume/events 运维排障流程(Taskiq + Redis + Last-Event-ID |
| 2026-03-06 | Web 启动从 gunicorn 迁移为纯 uvicorn,移除 `SOCIAL_WEB__GUNICORN__*` 配置,统一使用 `SOCIAL_WEB__WORKERS` |
| 2026-03-09 | 清理本地 Supabase 依赖描述:基础设施启动与巡检统一为 redis/db/web |