168 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.

# 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 调用时提供完整约束)*