# 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 命名空间）暴露 GET，user 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 候选优先级 #5（pytest 体系）落地后再做
- **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 调用时提供完整约束）*
