feat: harden auth and study workflows

- replace plaintext login and unlock requests with RSA-OAEP/AES-GCM encrypted payloads

- add login challenge replay protection, production RSA key validation, and auth tests

- wire compose to environment-driven dev/prod settings without committing local secrets

- update setup-config smoke scripts and Postman docs for encrypted login

- add visit schedule migrations/tests and update study/subject setup workflows
This commit is contained in:
Cheng Zhou
2026-05-08 22:13:12 +08:00
parent a7bbcaa5dc
commit 74feca4467
47 changed files with 2423 additions and 534 deletions
@@ -0,0 +1,328 @@
# CTMS 分支环境安装配置指南
本文定义 `dev``main``release` 三类分支对应的推荐部署环境、配置文件、初始化步骤与验证命令。分支本身不会自动切换运行模式,实际模式由后端环境变量 `ENV` 决定。
## 分支与环境映射
| 分支 | 分支定位 | 推荐环境 | 后端 `ENV` | 用途 |
| --- | --- | --- | --- | --- |
| `dev` | 日常开发与集成 | 开发环境 | `development` | 功能开发、联调、自测、内部集成 |
| `main` | 下一版本候选 | 预发布 / 验收环境 | `production` | 回归测试、验收、发布候选验证 |
| `release` | 当前稳定生产线 | 生产环境 | `production` | 正式生产部署、生产 hotfix |
推广路径保持为:
```text
feature/* -> dev -> main -> release
```
不要把 `release` 当作日常开发分支使用。`main` 应只接收已经准备进入候选版本范围的变更。
## 公共前置条件
所有环境都需要:
- Docker 与 Docker Compose
- 可写的 `pg_data/` 数据目录
- 端口 `8888``8000``5432` 未被占用
- 后端镜像可安装 `backend/requirements.txt` 中的依赖
- 前端镜像可执行 `npm ci``npm run build`
当前 compose 服务拓扑:
```text
nginx -> backend -> db
```
对外入口:
- 前端:`http://localhost:8888`
- 后端 API:同域 `/api/v1/*`
- 健康检查:`http://localhost:8888/health`
## dev 分支:开发环境
`dev` 默认用于本地开发和内部集成,推荐使用 `ENV=development`
### 1. 切换分支
```bash
git checkout dev
git pull --rebase origin dev
```
### 2. 配置 `.env`
根目录 `.env` 不提交到仓库。开发环境推荐:
```env
COMPOSE_PROJECT_NAME=ctms_dev
ENV=development
JWT_SECRET_KEY=dev-secret
LOGIN_RSA_KEY_ID=default
LOGIN_RSA_PRIVATE_KEY=
```
说明:
- `ENV=development` 会启用开发行为。
- 未配置 `LOGIN_RSA_PRIVATE_KEY` 时,后端启动后会生成临时 RSA 私钥。
- 后端重启后临时公钥会变化,已有登录页应刷新后重新登录。
- `JWT_SECRET_KEY=dev-secret` 只允许本地开发使用。
### 3. 启动
```bash
docker compose up -d --build backend nginx
```
如果需要首次启动完整栈:
```bash
docker compose up -d --build
```
### 4. 开发模式行为
`development` 模式下:
- FastAPI `debug=True`
- 应用启动时会执行 `Base.metadata.create_all`
- 应用启动时会确保默认管理员存在
- 未配置 RSA 私钥时允许临时生成
这些行为只适合开发,不适合生产。
### 5. 验证
```bash
docker compose ps
docker compose exec backend python -c "from app.core.config import settings; print(settings.ENV)"
curl -i http://127.0.0.1:8888/health
curl -i http://127.0.0.1:8888/api/v1/auth/login-key
```
预期:
```text
ENV=development
GET /health -> 200
GET /api/v1/auth/login-key -> 200, key_id=default
```
## main 分支:预发布 / 验收环境
`main` 是下一正式版本候选分支,应按生产模式运行,但不直接承载正式生产流量。
### 1. 切换分支
```bash
git checkout main
git pull --rebase origin main
```
### 2. 配置 `.env`
预发布环境应使用 `ENV=production`,并配置独立于生产的密钥:
```env
COMPOSE_PROJECT_NAME=ctms_staging
ENV=production
JWT_SECRET_KEY=<staging-strong-random-secret>
LOGIN_RSA_KEY_ID=staging-YYYYMMDD
LOGIN_RSA_PRIVATE_KEY=<staging-rsa-private-key-pem-with-\n-escaped-newlines>
```
生成密钥示例:
```bash
openssl rand -hex 32
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048
```
要求:
- `JWT_SECRET_KEY` 不得使用 `dev-secret`
- `LOGIN_RSA_PRIVATE_KEY` 必须固定保存,不能每次部署重新生成。
- staging 私钥不得复用 production 私钥。
### 3. 初始化数据库
生产模式不会自动建表或自动补管理员。首次部署或迁移前执行:
```bash
docker compose run --rm backend-init
```
该步骤应运行 Alembic migration,并确保固定管理员账号存在。
### 4. 启动
```bash
docker compose up -d --build
```
### 5. 验证
```bash
docker compose config
docker compose ps
docker compose exec backend python -c "from app.core.config import settings; print(settings.ENV); print(settings.JWT_SECRET_KEY == 'dev-secret'); print(bool(settings.LOGIN_RSA_PRIVATE_KEY))"
curl -i http://127.0.0.1:8888/health
curl -i http://127.0.0.1:8888/api/v1/auth/login-key
```
预期:
```text
ENV=production
JWT_SECRET_KEY == dev-secret -> False
LOGIN_RSA_PRIVATE_KEY_SET -> True
GET /health -> 200
GET /api/v1/auth/login-key -> 200
```
### 6. 验收门禁
在把 `main` 推进到 `release` 前,至少执行:
```bash
docker compose run --rm -v "$PWD/backend/tests:/code/tests:ro" backend python -m pytest
cd frontend && npm run test:unit
cd frontend && npm run type-check
cd frontend && npm run build
```
## release 分支:生产环境
`release` 是正式生产稳定分支。只有正式发布和生产 hotfix 应进入该分支。
### 1. 切换分支
```bash
git checkout release
git pull --rebase origin release
```
### 2. 配置 `.env`
生产环境必须使用生产专用配置:
```env
COMPOSE_PROJECT_NAME=ctms_prod
ENV=production
JWT_SECRET_KEY=<production-strong-random-secret>
LOGIN_RSA_KEY_ID=prod-YYYYMMDD
LOGIN_RSA_PRIVATE_KEY=<production-rsa-private-key-pem-with-\n-escaped-newlines>
```
要求:
- `.env` 必须只保存在部署机器或密钥管理系统中。
- 不得提交 `.env`、私钥、JWT 密钥。
- `JWT_SECRET_KEY` 轮换会使既有 token 失效,应安排维护窗口。
- `LOGIN_RSA_PRIVATE_KEY` 轮换会影响新登录密钥获取,应同步更新 `LOGIN_RSA_KEY_ID`
### 3. HTTPS 要求
登录加密依赖浏览器 WebCrypto。生产访问必须使用 HTTPS
- `https://正式域名`
- 本地 `localhost` 是浏览器安全上下文例外,但不能代表生产可用性
如果生产仍通过 `http://服务器IP:8888` 访问,前端会拒绝执行登录加密。
### 4. 初始化与迁移
首次部署或每次包含 migration 的发布:
```bash
docker compose run --rm backend-init
```
确认 migration 成功后再启动或滚动重启服务。
### 5. 启动
```bash
docker compose up -d --build
```
### 6. 生产验证
```bash
docker compose ps
docker compose exec backend python -c "from app.core.config import settings; print(settings.ENV); print(settings.JWT_SECRET_KEY == 'dev-secret'); print(bool(settings.LOGIN_RSA_PRIVATE_KEY)); print(settings.LOGIN_RSA_KEY_ID)"
curl -i https://<production-domain>/health
curl -i https://<production-domain>/api/v1/auth/login-key
```
预期:
```text
ENV=production
JWT_SECRET_KEY == dev-secret -> False
LOGIN_RSA_PRIVATE_KEY_SET -> True
GET /health -> 200
GET /api/v1/auth/login-key -> 200
```
### 7. 多实例约束
当前登录 challenge 默认保存在后端进程内:
- 单实例、单 worker:可直接使用
- 多实例或多 worker:必须满足以下至少一项
- 使用共享缓存保存 challenge,例如 Redis
- 启用粘性会话,确保 `/login-key``/login` 命中同一后端进程
- 将后端限制为单 worker 单副本
如果不满足,上线后可能出现偶发登录失败。
## 环境变量说明
| 变量 | dev | main/staging | release/production | 说明 |
| --- | --- | --- | --- | --- |
| `COMPOSE_PROJECT_NAME` | `ctms_dev` | `ctms_staging` | `ctms_prod` | 防止不同环境容器名、网络名冲突 |
| `ENV` | `development` | `production` | `production` | 后端运行模式 |
| `JWT_SECRET_KEY` | `dev-secret` | 强随机 | 强随机 | JWT 签名密钥 |
| `LOGIN_RSA_KEY_ID` | `default` | `staging-YYYYMMDD` | `prod-YYYYMMDD` | 登录 RSA 密钥版本 |
| `LOGIN_RSA_PRIVATE_KEY` | 空 | staging 私钥 | production 私钥 | RSA 私钥,生产模式必填 |
## 常见问题
### 登录页提示当前浏览器环境不支持安全登录加密
原因:非 HTTPS、非 localhost 的访问环境不满足 WebCrypto 安全上下文要求。
处理:
- 本地使用 `http://localhost:8888`
- staging/production 使用 HTTPS 域名
### production 模式启动失败,提示必须配置 LOGIN_RSA_PRIVATE_KEY
原因:`ENV=production` 时必须提供固定 RSA 私钥。
处理:
```bash
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048
```
将输出 PEM 写入 `.env``LOGIN_RSA_PRIVATE_KEY`,换行使用 `\n` 转义。
### 切换分支后环境不符合预期
分支不会自动修改 `.env`。切换分支后应重新确认:
```bash
docker compose config
docker compose exec backend python -c "from app.core.config import settings; print(settings.ENV)"
```
必要时修改 `.env` 并重启:
```bash
docker compose up -d --build backend nginx
```