11 KiB
11 KiB
Phase 3:客户端读取与日志脱敏 - Context
Gathered: 2026-05-08
Status: Ready for planning
Source: 用户在 /gsd-plan-phase 3 调用时提供的内联约束(PRD 快速通道,与 Phase 1/2 同模式)
本 phase 是 Milestone v1.0 的收尾 phase,负责:
- 客户端 GET 接口(CRED-05):在
/api/credential-slot/(非 admin 命名空间)暴露 GET,user token 鉴权(token:{token}Redis key),明文返回供手机/设备端 Unity 实际调用第三方服务 - 阿里云日志脱敏(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/全仓找到现有客户端接口的路由汇总点。候选:- 候选 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 文件路径与一行注册代码
- 候选 A:
- View 类:
CredentialSlotClientView(命名与 Phase 2 的CredentialSlotAdminView形成对称) - View 实现:自定义
APIView+ 仅get方法(不要 GET/PUT 双方法 —— 客户端只读) - View 放置位置:
aiapp/views.py末尾(与CredentialSlotAdminView紧邻),保持单 app 内同语义聚合 - 鉴权:
authentication_classes = [RedisTokenAuthentication]—— 复用 Phase 2 已 importpermission_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(推荐,最低侵入):
- 在
common/logging/filters.py(如不存在则新建)实现AccessTokenMaskFilter(logging.Filter) filter(record)方法:用正则扫描record.msg+record.args(dict / tuple / str),替换access_token字段值为mask_token(value)输出- 覆盖路径:在
LOGGING.filters注册该 filter,在LOGGING.handlers.aliyun/console等 handler 上配置filters: ['access_token_mask'] - 优点:所有日志路径(middleware request/response 日志、view 内 logger 日志、Django 默认 access log)统一覆盖
- 缺点:正则匹配
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) - 不引入新依赖
<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—CredentialSlotSerializerqy_lty/aiapp/views.py—CredentialSlotAdminView(Phase 2 模板,照搬结构但行为反向)qy_lty/userapp/admin_urls.py— admin 命名空间的注册(对照参考,本 phase 走客户端命名空间)qy_lty/common/utils.py—mask_tokenqy_lty/common/responses.py—success_response/error_responseqy_lty/common/swagger_utils.py—get_standardized_response_schemaqy_lty/userapp/authentication.py—RedisTokenAuthenticationqy_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>
## 具体要点(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 互引 |
- 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
Phase: 03-client-and-log-mask Context gathered: 2026-05-08 via inline PRD(用户在 /gsd-plan-phase 3 调用时提供完整约束)