docs(03): 从用户内联约束生成 Phase 3 CONTEXT.md(客户端 GET 明文 + 阿里云日志脱敏 PRD 快速通道)

This commit is contained in:
pmc 2026-05-08 09:33:26 +08:00
parent cf2477e738
commit 5a57f91324

View File

@ -0,0 +1,167 @@
# Phase 3客户端读取与日志脱敏 - Context
**Gathered**: 2026-05-08
**Status**: Ready for planning
**Source**: 用户在 `/gsd-plan-phase 3` 调用时提供的内联约束PRD 快速通道,与 Phase 1/2 同模式)
<domain>
## Phase 边界
本 phase 是 Milestone v1.0 的**收尾 phase**,负责:
1. **客户端 GET 接口**CRED-05`/api/credential-slot/`**非** admin 命名空间)暴露 GETuser token 鉴权(`token:{token}` Redis key**明文**返回供手机/设备端 Unity 实际调用第三方服务
2. **阿里云日志脱敏**CRED-06在 logging 链路formatter / filter / handler识别 `access_token` 字段并脱敏,覆盖管理端 PUT 请求体 / 管理端 GET 响应体 / 客户端 GET 响应体三条最易泄露路径
**与 Phase 2 的关键差异**
- Phase 2 是管理端admin token + view 层脱敏 + 双端互引修改记录)
- Phase 3 是客户端user token + view 层**明文** + 日志层脱敏)
- 同一份 `CredentialSlot` 单例,**view 行为相反**admin 脱敏 / client 明文)
**不负责**(推迟至 v2.x 或独立 milestone
- DB at-rest 加密(运营仍可在 admin 看到明文录入态)
- token 轮换 / 短长 access/refresh token 双轨
- 客户端调用频率限流
- 与 Unity 客户端的 SDK 联调(在 `LTY_Project` / `LTY_App_Project_URP` 独立仓库进行)
- 跨项目联动修改记录Unity 客户端在独立 repo不涉及 qy-lty-admin 前端,**不**写互引)
</domain>
<decisions>
## 实现决策(锁定)
### CRED-05客户端 GET 接口
- **路径**`/api/credential-slot/`(与管理端 `/api/v1/admin/credential-slot/` **完全分开**;客户端走 `/api/` 一级命名空间,避免被 admin token 限制误拒)
- **HTTP 方法**:仅 GET客户端不能写
- **路由注册位置**planner 必须 grep `/api/` 全仓找到现有客户端接口的路由汇总点。候选:
- 候选 A`qy_lty/urls.py` 已有 `path('api/', include(...))` 总入口;本 phase 在该 include 链下找一个最合适的子 urls
- 候选 B在 aiapp 自己 `aiapp/urls.py``path('credential-slot/', ...)`,再确认它是否已被 `qy_lty/urls.py``path('api/', include('aiapp.urls'))` 收容
- 候选 C参考现有客户端接口`/api/user/...``/api/device/...``/api/ai/...`)的注册位置照抄
- **planner 在 read_first 阶段必须把这 3 个候选挨个验证,给出明确选定的 urls.py 文件路径与一行注册代码**
- **View 类**`CredentialSlotClientView`(命名与 Phase 2 的 `CredentialSlotAdminView` 形成对称)
- **View 实现**:自定义 `APIView` + 仅 `get` 方法(**不要** GET/PUT 双方法 —— 客户端只读)
- **View 放置位置**`aiapp/views.py` 末尾(与 `CredentialSlotAdminView` 紧邻),保持单 app 内同语义聚合
- **鉴权**
- `authentication_classes = [RedisTokenAuthentication]` —— 复用 Phase 2 已 import
- `permission_classes = [IsAuthenticated]`
- **不**做 `is_staff` 校验(手机/设备 user 没有 staff 权限)
- `RedisTokenAuthentication` 通过 `userapp/utils.py:get_user_id_from_token` 自动识别 admin/user token但本 view **不区分**两种 token —— 都允许admin 用户也是手机用户的超集,让 admin 用 user token 能访问也合理;不构成安全问题,因为响应数据相同)
- **响应序列化器****直接复用** Phase 2 的 `CredentialSlotSerializer`(同 app 同字段);客户端 view 只是不调 `mask_token` 替换,所以是 `success_response(data=serializer.data)` 直返
- **响应壳层**:用 `common.responses.success_response`(与 Phase 2 一致)
- **Swagger**method-level `@swagger_auto_schema` + `get_standardized_response_schema`response description 标注「**明文** APP ID + Access Token供手机/设备端实际调用第三方服务」
### CRED-06阿里云日志脱敏
**实现策略选择**
仓库现有日志架构researcher 必须 read_first 实证):
- `qy_lty/settings.py` 中的 `LOGGING` 配置块
- `common/logging/` 目录(如存在)或 `aliyun_log_python_sdk` 集成位置
- 关键问题:日志写入是用 Python 标准 `logging.Logger` + 自定义 handler/formatter还是直接 SDK 调用?
**两种实现路径**planner 选其一并落地):
**路径 A — 自定义 logging.Filter**(推荐,最低侵入):
1. 在 `common/logging/filters.py`(如不存在则新建)实现 `AccessTokenMaskFilter(logging.Filter)`
2. `filter(record)` 方法:用正则扫描 `record.msg` + `record.args`dict / tuple / str替换 `access_token` 字段值为 `mask_token(value)` 输出
3. **覆盖路径**:在 `LOGGING.filters` 注册该 filter`LOGGING.handlers.aliyun` / `console` 等 handler 上配置 `filters: ['access_token_mask']`
4. **优点**所有日志路径middleware request/response 日志、view 内 logger 日志、Django 默认 access log统一覆盖
5. **缺点**:正则匹配 `access_token` 可能误伤无关字段(如某用户名恰好叫 `access_token`),但实际场景概率极低
**路径 B — 重写 StandardResponseMiddleware 的日志输出 + view 内 logger.info 显式脱敏调用**(侵入大,覆盖面窄,**不推荐**
- 仅推荐路径 A
**所以本 phase 走路径 A**:实现 `common/logging/filters.py:AccessTokenMaskFilter`,挂到 `settings.LOGGING.handlers` 的所有 handler 上。
**正则模式**planner 决定具体 regex建议覆盖以下 4 种序列化形态:
- JSON 字符串:`"access_token":\s*"([^"]+)"` → 替换 group(1) 为脱敏
- Python dict repr`'access_token':\s*'([^']+)'` → 替换 group(1) 为脱敏
- query string`access_token=([^&\s]+)` → 替换 group(1) 为脱敏
- 单独 token 值(极少见):暂不处理,靠上面三个序列化形态兜底
**留给 Claude's Discretion**
- filter 是用 `logging.Filter` 子类还是 `logging.Formatter` 子类(前者改 record后者改格式化输出两者效果等价前者更易测
- 是否同时在 `common/middleware.py:StandardResponseMiddleware` 加一道辅助保险response.body 写日志前显式脱敏)—— 视 researcher 看到 middleware 实际是否输出日志而定
### 跨 phase 契约
- 客户端 view 复用 `CredentialSlotSerializer`**不**新增 `CredentialSlotClientSerializer`)—— 字段集与管理端完全一致,避免分化
- `mask_token` 工具继续复用filter 内调用)
- `get_solo()` 类方法继续复用
### 兼容性 / 不引入新依赖
- 沿用 Django 4.2.13、Python 3.8、DRF 3.x、`aliyun-log-python-sdk`(如已在 requirements
- 不引入新依赖
</decisions>
<canonical_refs>
## Canonical References
**下游 agent 必读**
### 项目宪法
- `qy_lty/CLAUDE.md` — 沟通语言(中文)+ 修改记录强制规则
- `qy_lty/.planning/PROJECT.md` — Milestone v1.0「关键约束」段(明确客户端必须明文返回)
- `qy_lty/.planning/REQUIREMENTS.md` — Active 段 CRED-05 + CRED-06 完整描述
- `qy_lty/.planning/ROADMAP.md` — Phase 3 详情段Goal、Success Criteria 4 条)
### Phase 1 + Phase 2 已有产物(必读,对照 contract
- `qy_lty/aiapp/models.py``CredentialSlot` 单例模型 + `get_solo()`
- `qy_lty/aiapp/serializers.py``CredentialSlotSerializer`
- `qy_lty/aiapp/views.py``CredentialSlotAdminView`Phase 2 模板,照搬结构但行为反向)
- `qy_lty/userapp/admin_urls.py` — admin 命名空间的注册(**对照参考**,本 phase 走客户端命名空间)
- `qy_lty/common/utils.py``mask_token`
- `qy_lty/common/responses.py``success_response` / `error_response`
- `qy_lty/common/swagger_utils.py``get_standardized_response_schema`
- `qy_lty/userapp/authentication.py``RedisTokenAuthentication`
- `qy_lty/userapp/utils.py``get_user_id_from_token`admin/user token 区分逻辑,对照确认客户端走 user 路径)
### 客户端路由汇总点researcher 必须找出)
- `qy_lty/qy_lty/urls.py` — 顶层 urls`api/` namespace 入口
- 候选:`aiapp/urls.py` / `userapp/urls.py` / `device_interaction/urls.py` 之一是客户端接口聚合点
- 任一现有客户端接口(如 `/api/user/mac-login/``/api/ai/...`)作为 1:1 注册风格参考
### 日志架构researcher 必须找出)
- `qy_lty/qy_lty/settings.py``LOGGING` 配置块
- `qy_lty/common/logging/` 目录(如存在)
- 阿里云 SDK handler 类(如 `aliyun.log.QueuedLogHandler` 或自定义封装)
### 修改记录
- `qy_lty/docs/修改记录.md` — 顶部 Phase 1 + Phase 2 已有 4 条条目Phase 3 在最顶部追加新条目
</canonical_refs>
<specifics>
## 具体要点Success Criteria 显式化)
| # | 验证点 | 检查方式 |
|---|--------|----------|
| 1 | 客户端 GET 携 user token 返回明文壳层 | `curl -H "Authorization: Bearer <user_token>" /api/credential-slot/` 返回 200 + JSON `data.access_token == 'probe_secret_xxxx'`(明文,**非脱敏** |
| 2 | 无 token 返回 401 + 标准壳层 | `curl /api/credential-slot/` 返回 401 + JSON 含 `success: false` |
| 3 | 过期 / 无效 token 返回 401 | 用伪造 token 调用,`RedisTokenAuthentication` 拒绝 |
| 4 | 管理端 PUT 请求体在日志中脱敏 | 触发一次 `PUT /api/v1/admin/credential-slot/` 提交 `{"access_token": "test_log_secret_zzzz"}`,检查 `tail -100 <log file>` 不含 `test_log_secret_zzzz` 完整明文(应被替换为 `*****************zzzz` 或类似) |
| 5 | 管理端 GET 响应体已脱敏Phase 2 + 日志层双重保险) | 触发一次 admin GET日志文件搜索响应段不含完整明文 |
| 6 | 客户端 GET 响应明文不写入日志 | 触发一次 `GET /api/credential-slot/`(携 user token日志文件搜索请求/响应段不含完整 `probe_secret_xxxx` 明文(应被替换或不打印响应体)|
| 7 | 端到端往返一致 | admin PUT 写入 `{app_id: 'roundtrip_test', access_token: 'rt_secret_RT99'}` → client GET 返回 `{app_id: 'roundtrip_test', access_token: 'rt_secret_RT99'}`(明文一致)|
| 8 | Swagger 暴露客户端接口 | `/swagger.json/``/api/credential-slot/` 路径条目 + GET 方法 + 响应 schema description 标注「明文返回」 |
| 9 | 修改记录顶部追加 Phase 3 条目 | grep `qy_lty/docs/修改记录.md` 顶部出现 `[2026-05-08] Phase 3` 字样;不写 qy-lty-admin 互引 |
</specifics>
<deferred>
## 推迟事项(明确不在 Phase 3 范围)
- **DB at-rest 加密 access_token 字段** — 留 v2.x 评估
- **客户端调用频率限流** — 现架构未上限流
- **token 轮换 / refresh token 机制** — 留独立 milestone
- **Unity 客户端 SDK 联调** — 在 `LTY_Project` / `LTY_App_Project_URP` 独立仓库
- **生产日志脱敏自动化测试**CI 跑实日志验证)— 当前用本地 dev 日志手动验证CI 测试基础设施待 brownfield 候选优先级 #5pytest 体系)落地后再做
- **DEBUG / CORS_ALLOW_ALL_ORIGINS 收紧** — brownfield 候选优先级 #3,留独立 phase
</deferred>
---
*Phase: 03-client-and-log-mask*
*Context gathered: 2026-05-08 via inline PRD用户在 /gsd-plan-phase 3 调用时提供完整约束)*