11 KiB
Raw Blame History

Phase 3客户端读取与日志脱敏 - Context

Gathered: 2026-05-08 Status: Ready for planning Source: 用户在 /gsd-plan-phase 3 调用时提供的内联约束PRD 快速通道,与 Phase 1/2 同模式)

## 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 前端,写互引)
## 实现决策(锁定)

CRED-05客户端 GET 接口

  • 路径/api/credential-slot/(与管理端 /api/v1/admin/credential-slot/ 完全分开;客户端走 /api/ 一级命名空间,避免被 admin token 限制误拒)
  • HTTP 方法:仅 GET客户端不能写
  • 路由注册位置planner 必须 grep /api/ 全仓找到现有客户端接口的路由汇总点。候选:
    • 候选 Aqy_lty/urls.py 已有 path('api/', include(...)) 总入口;本 phase 在该 include 链下找一个最合适的子 urls
    • 候选 B在 aiapp 自己 aiapp/urls.pypath('credential-slot/', ...),再确认它是否已被 qy_lty/urls.pypath('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 一致)
  • Swaggermethod-level @swagger_auto_schema + get_standardized_response_schemaresponse 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.argsdict / tuple / str替换 access_token 字段值为 mask_token(value) 输出
  3. 覆盖路径:在 LOGGING.filters 注册该 filterLOGGING.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 stringaccess_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
  • 不引入新依赖

<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.pyCredentialSlot 单例模型 + get_solo()
  • qy_lty/aiapp/serializers.pyCredentialSlotSerializer
  • qy_lty/aiapp/views.pyCredentialSlotAdminViewPhase 2 模板,照搬结构但行为反向)
  • qy_lty/userapp/admin_urls.py — admin 命名空间的注册(对照参考,本 phase 走客户端命名空间)
  • qy_lty/common/utils.pymask_token
  • qy_lty/common/responses.pysuccess_response / error_response
  • qy_lty/common/swagger_utils.pyget_standardized_response_schema
  • qy_lty/userapp/authentication.pyRedisTokenAuthentication
  • qy_lty/userapp/utils.pyget_user_id_from_tokenadmin/user token 区分逻辑,对照确认客户端走 user 路径)

客户端路由汇总点researcher 必须找出)

  • qy_lty/qy_lty/urls.py — 顶层 urlsapi/ 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.pyLOGGING 配置块
  • qy_lty/common/logging/ 目录(如存在)
  • 阿里云 SDK handler 类(如 aliyun.log.QueuedLogHandler 或自定义封装)

修改记录

  • qy_lty/docs/修改记录.md — 顶部 Phase 1 + Phase 2 已有 4 条条目Phase 3 在最顶部追加新条目

</canonical_refs>

## 具体要点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 互引
## 推迟事项(明确不在 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

Phase: 03-client-and-log-mask Context gathered: 2026-05-08 via inline PRD用户在 /gsd-plan-phase 3 调用时提供完整约束)