Files
social-app/docs/plans/2026-03-09-cloud-supabase-jwks-migration-plan.md
T
qzl 6fe2e7b6c3 refactor: 迁移本地 Supabase 到云端,使用 JWKS 进行 JWT 验证
- 新增 JwtVerifier 支持 RS256 + JWKS 验证
- 简化 docker-compose,删除本地 Supabase 服务(kong/auth/storage等)
- 删除冗余的 Supabase 配置文件(volumes目录)
- 适配测试用例以支持新配置方式
- 更新运行时文档和迁移计划
2026-03-09 18:03:04 +08:00

304 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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"
```