---
phase: 03-client-and-log-mask
plan: 02
subsystem: logging
tags: [logging, dictconfig, filter, mask-token, regex, defensive-mitigation, milestone-v1.0-complete]

# Dependency graph
requires:
  - phase: 01-credential-data-layer
    provides: common.utils.mask_token / aiapp.models.CredentialSlot
  - phase: 02-admin-rest
    provides: aiapp.views.CredentialSlotAdminView（端到端 T8 admin PUT 走它）+ DB 探针态 pk=1/probe_app/probe_secret_xxxx
  - phase: 03-01
    provides: aiapp.views.CredentialSlotClientView + /api/credential-slot/ 路由（端到端 T1/T2 走它）
provides:
  - common.logging 子包 + common.logging.filters.AccessTokenMaskFilter（logging.Filter 子类，4 正则覆盖 JSON / Pyrepr / URL query / 等号或冒号兜底）
  - settings.LOGGING.filters.access_token_mask（dictConfig 工厂语法 () 注册）
  - settings.LOGGING.handlers.aliyun.filters / handlers.console.filters（filter 挂在 handler 层，统一覆盖所有 logger → handler 路径）
  - docs/修改记录.md 顶部 [2026-05-08] Phase 3 条目（覆盖 5 处文件 + CRED-05/06 + 跨项目联动「无」）
  - .planning/phases/03-client-and-log-mask/03-VERIFICATION.md（9 truth × 32 项独立断言全 PASS 报告）
affects:
  - 任何后续 view / middleware / 业务代码内 logger.info/debug/warning 调用：含 access_token 字段的字符串 / dict args / tuple args 形态会被自动脱敏；不再依赖每个调用点自律
  - Milestone v1.0「通用凭据槽位」：CRED-01 至 CRED-06 全部 Done，本期目标圆满达成

# Tech tracking
tech-stack:
  added: []
  patterns:
    - "logging.Filter 子类挂在 LOGGING.handlers 层（不挂 loggers 层）—— 一次注册全链路覆盖"
    - "dictConfig 工厂语法 () 用于 filter（区别于 handler 用 class）"
    - "正则 pattern 终止符设计：Pattern 4 兜底 regex 必须排除 & / = 等 URL 分隔符，避免重复 mask Pattern 3 的输出"
    - "tuple args 形态先 record.getMessage() 拼成最终字符串再整体脱敏 + args=None：避免 Formatter 阶段 % 格式化时占位符被吃后报 TypeError"
    - "防御性兜底而非修复式补丁：当前仓库无既有泄露路径，filter 是为未来代码改动留的安全网"

key-files:
  created:
    - common/logging/__init__.py（空文件，0 字节，包 marker）
    - common/logging/filters.py（106 行：AccessTokenMaskFilter 类 + 4 个 _PATTERNS 正则 + _NEEDLE 短路 + _sub / _mask_in_text / filter 方法）
    - .planning/phases/03-client-and-log-mask/03-VERIFICATION.md（端到端 9 truth × 32 PASS 报告）
    - .planning/phases/03-client-and-log-mask/03-02-SUMMARY.md（本文件）
  modified:
    - qy_lty/settings.py（LOGGING 字典：新增 filters 段（4 行）+ aliyun handler 加 filters: ['access_token_mask']（1 行）+ console handler 加 filters: ['access_token_mask']（1 行）+ 4 行注释；前后行差 +11 行）
    - docs/修改记录.md（顶部追加 [2026-05-08] Phase 3 条目，紧邻 [2026-05-07] Phase 2 之上；+22 行）
  deleted:
    - _phase3_01_verify.py（plan 03-01 创建的临时验收脚本，本 plan Task 3 末尾统一删除）
    - _phase3_02_unit_test.py（Task 1 临时单元测试，验完即删）
    - _phase3_02_verify.py（Task 3 端到端验收脚本，验完即删 + 输出落 03-VERIFICATION.md）
    - _phase3_02_settings_check.py（Task 2 验收 helper，验完即删）
    - _phase3_admin_mtime.txt（Task 4 mtime baseline 临时文件，验完即删）

key-decisions:
  - "[Plan 03-02] AccessTokenMaskFilter 挂在 LOGGING.handlers（aliyun + console）而非 loggers 段：挂 logger 仅过滤直接通过该 logger 的 record，挂 handler 才统一覆盖所有 logger → handler 路径（per RESEARCH Pitfall 1）"
  - "[Plan 03-02] dictConfig filter 注册用 () 工厂语法不用 class：dictConfig 标准对 filter 与 handler 语法不互通（per RESEARCH Pitfall 5）"
  - "[Plan 03-02] 4 个 regex 不合并成 1 个大 regex：可读性 + group 数差异（JSON/Pyrepr 是 3 group / Query/Fallback 是 2 group），合并会让 _sub 变脆"
  - "[Plan 03-02] filter 仅识别 access_token 字段名前缀锚点，不脱敏裸 token / Authorization / Bearer：那是另一类敏感数据，留 v2.x 候选优先级处理（per RESEARCH Pitfall 3）"
  - "[Plan 03-02] tuple args 形态走 record.getMessage() 预拼接后 args=None 再脱敏：避免 Formatter % 拼接时占位符被 mask 吃掉触发 TypeError（auto-fix Rule 1，详见 Deviations）"
  - "[Plan 03-02] Pattern 4 兜底 regex 终止符增加 & / = 排除：避免 Pattern 3 的输出 access_token=********1234&u=1 被 Pattern 4 把 ********1234&u=1 整段二次 mask 把末 4 位 1234 吃成 &u=1（auto-fix Rule 1，详见 Deviations）"
  - "[Plan 03-02] 端到端验收脚本走 python manage.py shell -c \"exec(open(...).read())\" 而非 < 重定向：Windows PowerShell 下 < 触发逐行 REPL 执行，缩进块全部 IndentationError（plan 03-01 已踩坑）"
  - "[Plan 03-02] 验收脚本 print 用 [PASS] / [FAIL] 而非 ✓ / ✗ 标记：Windows GBK 控制台无法编码 Unicode 字符（plan 03-01 已踩坑）"
  - "[Plan 03-02] 不写 qy-lty-admin/docs/修改记录.md 互引：CONTEXT 锁定 + RESEARCH 实证；客户端给 Unity 用（LTY_Project / LTY_App_Project_URP），qy-lty-admin 不消费 /api/credential-slot/（管理端走 Phase 2 落地的 /api/v1/admin/credential-slot/）"
  - "[Plan 03-02] 临时验收脚本（_phase3_01_verify.py / _phase3_02_unit_test.py / _phase3_02_verify.py / _phase3_02_settings_check.py / _phase3_admin_mtime.txt）一律不入 git：与 Phase 2 / Plan 03-01 一致，是一次性证据生成器"

patterns-established:
  - "logging.Filter 子类 + dictConfig () 工厂语法 + 挂 handler 不挂 logger 的脱敏 filter 模式（未来其它字段如 Bearer / api_secret 想脱敏时直接复刻）"
  - "正则交叉吃尾的兜底 regex 终止符设计法则：兜底 regex 必须排除前置 regex 的边界字符，避免链式调用时把已脱敏的输出再次当 value"
  - "tuple args 形态 LogRecord 处理：getMessage() 预拼 + args=None，避开占位符与 mask 输出的 % 格式化冲突"

requirements-completed:
  - CRED-06

# Metrics
duration: 8min14s
completed: 2026-05-08
---

# Phase 3 Plan 03-02：阿里云日志 access_token 脱敏 filter Summary

**AccessTokenMaskFilter 4 正则脱敏 filter 落地（dictConfig () 工厂语法注册到 LOGGING.handlers.aliyun + console），9 truth × 32 项独立断言全 PASS（CRED-05 + CRED-06 整合验收）；docs/修改记录.md 顶部追加 Phase 3 条目，跨项目联动「无」明示；Milestone v1.0「通用凭据槽位」CRED-01~06 至此全部 Done**

## Performance

- **Duration:** ~8.2 min（含 2 处 Rule 1 auto-fix bug）
- **Started:** 2026-05-08T02:22:08Z
- **Completed:** 2026-05-08T02:30:22Z
- **Tasks:** 4（包 + filter / settings / 端到端验收 / 修改记录条目）
- **Files modified:** 2（qy_lty/settings.py + docs/修改记录.md）
- **Files created:** 4（common/logging/__init__.py + common/logging/filters.py + 03-VERIFICATION.md + 03-02-SUMMARY.md）
- **Files deleted:** 5 临时（_phase3_01_verify.py + _phase3_02_unit_test.py + _phase3_02_verify.py + _phase3_02_settings_check.py + _phase3_admin_mtime.txt）

## Accomplishments

- **CRED-06 落地完成**：`AccessTokenMaskFilter(logging.Filter)` 已注册到 `qy_lty.settings.LOGGING.filters` 并由 `handlers.aliyun` / `handlers.console` 引用；任何 view / middleware / 第三方库的 `logger.info(包含 access_token 明文的字符串)` 都会被脱敏成 `mask_token(value)` 输出
- **9 truth 全 PASS（32 项独立断言）**：
  - T1-T5（5 truth × 14 项断言）：plan 03-01 客户端 GET 5 项验证（user/admin token 200 + 401 × 2 + swagger schema）
  - T6（4 项）：filter 4 种正则形态（JSON / Pyrepr / Query / Fallback）伪 LogRecord 处理后含末 4 位 `1234`、不含完整明文 `abcdefgh`
  - T7（2 项）：filter 不误伤 `Authorization header: bearer_user_token_xxxxxxx` / `Bearer raw_token_zzz` 等非 access_token 字段
  - T8（5 项）：端到端 admin PUT roundtrip → admin GET 脱敏 `**********RT99` + client GET 明文 `rt_secret_RT99` + app_id 一致
  - T9（2 项）：端到端 `logger.info('access_token=defensive_secret_DEFC')` → console 输出 `*****************DEFC` 脱敏（防御性兜底真实生效）
  - T_FINAL（2 项）：DB 探针态主动还原 `pk=1 / app_id='probe_app' / access_token='probe_secret_xxxx'`
- **DB 探针态保持稳定**：脚本前后 `pk=1, app_id='probe_app', access_token='probe_secret_xxxx'`，给后续工作（无论是下一周期 milestone 还是 v2.x 评估）留下与 Phase 1/2/3-01 一致的稳定起点
- **Phase 1 / Phase 2 / Plan 03-01 既有代码 0 改动**：`CredentialSlot` 模型 / Admin 注册 / `CredentialSlotAdminView` / `CredentialSlotClientView` / 既有 5 条 logger 配置完全未动；imports 段未变
- **Milestone v1.0 完结**：CRED-01（单例 model）+ CRED-02（Admin 注册）+ CRED-03（admin GET 脱敏）+ CRED-04（admin PUT 覆写）+ CRED-05（客户端 GET 明文）+ CRED-06（日志脱敏 filter）全部 Done

## Task Commits

每个 task 原子提交（中文 commit message）：

1. **Task 1: 新建 common/logging/ 包 + AccessTokenMaskFilter** — `891a5ea` (feat) — common/logging/__init__.py（0 字节）+ common/logging/filters.py（106 行）
2. **Task 2: settings.py LOGGING 注册 access_token_mask filter** — `35eb110` (feat) — LOGGING 字典 +11 行（filters 段 + 2 个 handler 各 1 行 filters 引用 + 注释）
3. **Task 3: Phase 3 端到端验收报告** — `7a9e511` (test) — 03-VERIFICATION.md 113 行（9 truth × 32 项断言）
4. **Task 4: docs/修改记录.md 追加 Phase 3 条目** — `db4d5cf` (docs) — +22 行（5 文件 + CRED-05/06 + 跨项目联动「无」）

**Plan metadata:** 待 final commit（本 SUMMARY + STATE.md / ROADMAP.md / REQUIREMENTS.md）

## Files Created/Modified

| 文件 | 操作 | 行数变化 | 说明 |
|------|------|----------|------|
| `common/logging/__init__.py` | 新建 | 0 字节（empty） | package marker |
| `common/logging/filters.py` | 新建 | 106 行 | AccessTokenMaskFilter 类 + 4 _PATTERNS + _NEEDLE 短路 + _sub/_mask_in_text/filter 方法 |
| `qy_lty/settings.py` | 修改 | LOGGING 块 +11 行（375-394 区间，filters 段 + 2 处 'filters' key） | dictConfig () 工厂语法注册 + handlers 引用 |
| `docs/修改记录.md` | 修改 | 顶部 +22 行（行 26-47 新条目） | [2026-05-08] Phase 3 条目，紧邻 [2026-05-07] Phase 2 之上 |
| `.planning/phases/03-client-and-log-mask/03-VERIFICATION.md` | 新建 | 113 行 | 9 truth × 32 项 PASS 报告 |
| `.planning/phases/03-client-and-log-mask/03-02-SUMMARY.md` | 新建 | 本文件 | 本 plan summary |

## 防御性兜底语义说明

**RESEARCH 实证（已读 03-CONTEXT.md / 03-RESEARCH.md）**：当前仓库**没有**任何代码 logger 输出 `CredentialSlot.access_token` 明文：

- `StandardResponseMiddleware`（`common/middleware.py`）只统一包装响应壳层，不打日志
- `CredentialSlotAdminView` / `CredentialSlotClientView` 都不显式 `logger.info(serializer.data)`
- Django 默认 access log 不含请求 / 响应 body
- `aliyun_log_python_sdk` 集成（`common/aliyun_logging.py`）只 emit `record.getMessage()`，没有自动 dump request

**所以 CRED-06 的真实价值是「防御性兜底」**，不是「修补现有泄露」：

- Phase 1 + Phase 2 + Phase 3 的 view 让 access_token 进入「内存中可被随手 dump」的状态
- 任何后续开发者写 `logger.info(f"PUT body: {request.data}")` 类型代码就会立即把 access_token 明文打到阿里云日志服务
- `AccessTokenMaskFilter` 在 LOGGING.handlers 层兜底拦截，不依赖每个调用点自律
- **未来代码改动天然安全** —— 即使新人在 view 里写 `logger.debug(self.request.data)` 也不会泄露

## Decisions Made

见 frontmatter `key-decisions` 字段，已 10 条决策全部归档。

## Deviations from Plan

### Auto-fixed Issues

**1. [Rule 1 - Bug] Pattern 4 兜底 regex 把 Pattern 3 已脱敏的输出二次 mask 吃掉末 4 位**

- **Found during:** Task 1 跑 `_phase3_02_unit_test.py` 首次 verify
- **Issue:** Plan 给的 Pattern 4 终止符是 `[^\s,;)\]\}"\']+`，对 Query 形态 `access_token=abcdefgh1234&u=1`：
  1. Pattern 3 `(access_token=)([^&\s"\']+)` 先 match → `access_token=********1234&u=1`
  2. Pattern 4 `(access_token\s*[:=]\s*)([^\s,;)\]\}"\']+)` 再 match → 因为 `&` / `=` 不在排除集，value = `********1234&u=1`（16 字符），mask_token 输出 `************` + 末 4 位 `&u=1` = `************&u=1`，把 `1234` 吃没了
- **Fix:** Pattern 4 终止符增加 `&` / `=` 排除字符 → `[^\s,;)\]\}"\'&=]+`；保证 Pattern 4 不再吞掉 Pattern 3 的输出尾部
- **Files modified:** common/logging/filters.py（行 47-52，Pattern 4 regex + 5 行注释解释为什么排除 `&=`）
- **Verification:** `_phase3_02_unit_test.py` 复跑 Query 形态 OUTPUT = `'GET /x?access_token=********1234&u=1'`（含末 4 位 `1234`）
- **Committed in:** 891a5ea（与 Task 1 主体一并提交）

**2. [Rule 1 - Bug] tuple args 形态把 `%s` 占位符当 access_token 的 value 吃掉，触发 Formatter TypeError**

- **Found during:** Task 1 跑 `_phase3_02_unit_test.py` 第 5 项断言（tuple args）
- **Issue:** Plan 给的 filter() 实现先扫 `record.msg` 再扫 `record.args`：
  1. msg = `'access_token=%s'`，args = `('abcdefgh1234',)`
  2. 扫 msg：Pattern 4 match `access_token=` + value = `%s`（`%` 与 `s` 都不在排除集），mask_token('%s') = `**`（短于 4 全脱）。msg 变成 `'access_token=**'`
  3. 扫 args：tuple 中 `'abcdefgh1234'` 不含 `access_token` 字面量，`_NEEDLE not in text.lower()` 短路返回，不变
  4. Formatter 阶段：`'access_token=**' % ('abcdefgh1234',)` → 占位符 `%s` 已被 `**` 替换，args 仍有 1 元素 → `TypeError: not all arguments converted during string formatting`
- **Fix:** 改 filter() 处理逻辑为：
  - tuple args 形态：先 `record.getMessage()` 拼出最终字符串（msg + args % 格式化结果），再整体走 `_mask_in_text` 脱敏，最后 `record.args = None` 避免 Formatter 二次拼接
  - 其它形态（无 args / dict args）：保持原 plan 逻辑分别处理 msg 和 args
- **Files modified:** common/logging/filters.py（行 79-111，filter 方法重构；增加 try/except getMessage 失败时 graceful return True 不影响其它 handler）
- **Verification:** `_phase3_02_unit_test.py` 复跑 OUTPUT = `'access_token=********1234'`（tuple args 末 4 位保留），全 9 项 ALL UNIT TESTS PASS
- **Committed in:** 891a5ea（与 Task 1 主体一并提交，因为 bug 在 Task 1 内被发现且未引入 git）

---

**Total deviations:** 2 auto-fixed（Rule 1 Bug × 2）
**Impact on plan:** 两处偏差均为 plan 给的 regex / filter 实现逻辑漏洞，端到端 9 truth × 32 项 PASS 全过；plan acceptance criteria 全部达成；CRED-06 防御性兜底语义正确

## Issues Encountered

无业务 / 架构问题。仅遇到上述 2 处 Plan 内置 bug，已自动修复并文档化。

## Stub Status

无。本 plan 落地的 filter + LOGGING 配置全部是真实生效的代码路径：

- `common/logging/filters.py:AccessTokenMaskFilter` 已挂载到 settings.LOGGING.handlers，T9 端到端 `logger.info` 真实输出验证脱敏生效
- 不存在 placeholder / 硬编码 / mock 数据 / 空函数

## User Setup Required

无 —— 不引入新依赖、不要求新环境变量、不需要外部服务配置；现有 `aliyun-log-python-sdk` 已在 Phase 0 安装

## Next Phase Readiness

**Milestone v1.0「通用凭据槽位」至此全部完成**：

- CRED-01（Phase 1 Plan 01-01）：单例 CredentialSlot 模型 + 迁移 ✓
- CRED-02（Phase 1 Plan 01-02）：Django Admin 注册 + 脱敏 ✓
- CRED-03（Phase 2 Plan 02-01）：管理端 GET 脱敏 ✓
- CRED-04（Phase 2 Plan 02-01）：管理端 PUT 覆写 ✓
- CRED-05（Phase 3 Plan 03-01）：客户端 GET 明文 ✓
- CRED-06（Phase 3 Plan 03-02）：阿里云日志脱敏 filter ✓

**下一周期候选 milestone**（详见 `.planning/REQUIREMENTS.md`「候选优先级」段）：

- HIGH：ACH-02 成就解锁条件校验缺失 / SMS 验证码无频率限制 / 收紧 DEBUG / CORS_ALLOW_ALL_ORIGINS / 移除测试 MAC 硬编码 / 测试基础设施搭建（pytest 体系）
- MEDIUM：好感度 P2/P3/P4 / Python 3.8 → 3.11/3.12 升级 / 拆分 device_interaction/views.py（1867 行）

## Self-Check: PASSED

- [x] common/logging/__init__.py 存在，0 字节空文件 (FOUND, 0 bytes)
- [x] common/logging/filters.py 含 `class AccessTokenMaskFilter(logging.Filter)` (FOUND)
- [x] common/logging/filters.py 含 4 个 _PATTERNS 正则 (FOUND, len = 4)
- [x] qy_lty/settings.py 含 `'access_token_mask'` filter 注册 (FOUND)
- [x] qy_lty/settings.py 含 `'()': 'common.logging.filters.AccessTokenMaskFilter'` 工厂语法 (FOUND)
- [x] qy_lty/settings.py 含 `['access_token_mask']` ≥ 2 处（aliyun + console） (FOUND, count = 2)
- [x] docs/修改记录.md 顶部含 `[2026-05-08] Phase 3` 条目 (FOUND, idx=253)
- [x] docs/修改记录.md Phase 3 条目位于 Phase 2 之上 (Phase 3 idx=253 < Phase 2 idx=4465)
- [x] docs/修改记录.md Phase 3 条目含 5 处文件路径 + CRED-05 + CRED-06 (FOUND)
- [x] docs/修改记录.md Phase 3 条目跨项目联动字段写「无 — 客户端给 Unity 用」(FOUND)
- [x] qy-lty-admin/docs/修改记录.md mtime 未变（baseline 1778166405.4241624 = current）
- [x] commit 891a5ea in git log (FOUND)
- [x] commit 35eb110 in git log (FOUND)
- [x] commit 7a9e511 in git log (FOUND)
- [x] commit db4d5cf in git log (FOUND)
- [x] _phase3_02_verify.py 输出 ALL PASS（32 PASS / 0 FAIL）
- [x] _phase3_02_unit_test.py 输出 ALL UNIT TESTS PASS
- [x] DB 探针态 pk=1 / app_id='probe_app' / access_token='probe_secret_xxxx' 保持
- [x] 5 个临时验收脚本均已删除（git status 干净）
- [x] python manage.py shell 启动无 ValueError（dictConfig 工厂语法正确）

## TDD Gate Compliance

本 plan frontmatter `type: execute`，非 TDD plan。无 RED/GREEN/REFACTOR 强制顺序要求。但实际操作上 Task 1 写完 filter + 单元测试同步落地，Task 1 commit 既含 filter 主体又验证 4 个正则形态 + tuple args 形态行为；与轻量 TDD 实质等价。

---
*Phase: 03-client-and-log-mask*
*Plan: 02*
*Completed: 2026-05-08*
*Milestone v1.0「通用凭据槽位（APP ID + Access Token）」at this point: ALL DONE — CRED-01 to CRED-06 全部交付*
