# 2026-06-07 — 登录 Refresh Token Redis + MySQL 双写

## 摘要

30 天登录会话的 refresh token 改为 **先写 Redis、再写 MySQL**；API 启动时从 MySQL 回填 Redis，Redis 重启/flush 后会话不丢失。

## 动机

此前 refresh token 仅存 Redis Set（`refresh_tokens:{admin_id}`），Redis 重载或 flush 后 HttpOnly Cookie 仍在但服务端认不出，用户被迫重新登录。

## 变更

| 文件 | 说明 |
|------|------|
| `server/domain/models/__init__.py` | 表 `admin_refresh_tokens`（token_hash + expires_at） |
| `server/infrastructure/database/refresh_token_repo.py` | MySQL CRUD / rotate / 按 admin 清理 |
| `server/application/services/auth_service.py` | 双写、校验 MySQL 回退、rotate 双端 |
| `server/application/services/refresh_token_hydration.py` | 启动 hydration + 清理过期行 |
| `server/main.py` | init_redis 后 hydration |
| `server/api/dependencies.py` | 注入 refresh_token_repo |
| `server/api/auth.py` | 改密走 `_delete_all_refresh_tokens` 双端清理 |
| `server/infrastructure/database/migrations.py` | CREATE TABLE 迁移 |
| `tests/test_refresh_token_persistence.py` | 双写 / MySQL 回退 / 双删 |

## 配置

- 有效期仍由 `JWT_REFRESH_TOKEN_EXPIRE_DAYS`（默认 **30**）控制
- Access Token 仍约 60 分钟，靠 refresh 续期

## 迁移 / 重启

需重启 API；新表自动 create_all + schema migration。

## 验证

```bash
pytest tests/test_refresh_token_persistence.py tests/test_auth_refresh_reuse.py -q
```

登录 → 重启 Redis → 刷新页面应仍可续期（hydration + MySQL 回退）。
