docs: 发版前评审/安全审计报告、迁移 runbook、CLAUDE.md 表格口径更新

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JJ1g8XV1YhhmHRzhwWEW7o
This commit is contained in:
wangjia
2026-07-03 09:58:30 +08:00
parent 824992fe6e
commit d5b11c0943
11 changed files with 2402 additions and 1 deletions
+102
View File
@@ -0,0 +1,102 @@
# 安全审计报告 — 2026-07-03
**审计范围**:后端 `backend/`router / 全部 handler / middleware / service / config / model)与部署链路(`.gitea/workflows/*``scripts/ci/*``deploy/*`)。重点核查本会话新增的 `summaryBounds`/`rolling30`(出入库 KPI)、`finance.Trend`、库存/出入库搜索的 `EXISTS/IN` 子查询与 LIKE 拼接、`?window` 参数处理、新增路由鉴权、CI secrets 使用、`backup-db.sh` 密码处理、公开端点暴露面。
**发现问题**Critical 1High 2Medium 3Low 4
---
## Critical — 必须立即修复
### SEC-001: 关联对象(Partner / Warehouse)跨租户信息泄露
**文件**
- `backend/internal/model/stock.go:26-27,80-81``backend/internal/model/finance.go:20`(关联定义只按 `foreignKey`,无 shop 作用域)
- `backend/internal/handler/stock_in.go:208-212``Get` Preload Partner/Warehouse
- `backend/internal/handler/stock_out.go:142-144``Get` Preload Partner/Warehouse
- `backend/internal/handler/finance.go:70``ListRecords` Preload Partner)、`finance.go:232-245``Summary` `LEFT JOIN partners p ON p.id = f.partner_id`,无 `p.shop_id` 约束)
- 写入侧不校验归属:`stock_in.go:220-268`Create 直接透传 `req.PartnerID`/`req.WarehouseID`)、`stock_out.go:190-239``finance.go:81-127`Create 透传 `partner_id`
**描述**:下单/建财务记录接口接收请求体里的 `partner_id``warehouse_id`,**不校验这些 ID 是否属于当前门店**。读取时用 GORM `Preload("Partner")` / `Preload("Warehouse")`(或 finance Summary 的 `LEFT JOIN partners`)按外键直接回连,**没有再按 `shop_id` 二次过滤**。因此攻击者能引用其他门店的 `partner_id`/`warehouse_id`,读回时把对方数据带出来。
**攻击场景**:攻击者登录 A 店,POST 一张草稿入库单 `{"warehouse_id":1,"partner_id":<枚举 N>,...}`,随后 GET `/api/v1/stock-in/orders/:id`,响应中的 `partner` 对象会返回 B 店该往来单位的 `name / contact / phone / address / bank_account / credit_limit / balance`(见 `model/partner.go` 字段)。循环枚举 `partner_id` 即可导出全平台所有门店的客户/供应商联系人、电话、地址、银行账号——多租户隔离被彻底击穿,且泄露 PII。finance `ListRecords`/`Summary` 同理会带出他店往来单位名。
**修复建议**
1. 写入侧:Create/Update 时校验 `partner_id``warehouse_id` 归属当前 `shop_id``SELECT 1 FROM partners WHERE id=? AND shop_id=?`),不符则 400。
2. 读取侧纵深防御:Preload 加租户条件,如 `Preload("Partner", "shop_id = ?", shopID)``Preload("Warehouse", "shop_id = ?", shopID)`finance Summary/ListRecords 的 join/preload 同样补 `p.shop_id = f.shop_id`
3. 同类需一并修:`inventory.CreateCheck`item.ProductID 未验归属)→ `GetCheck`/`CompleteCheck``Preload("Items.Product")` 会带出他店 product。
**验证方式**:用 A、B 两店 tokenA 建单引用 B 的 partner_idGET 单据确认 `partner` 为 null 或 400,而非返回 B 店数据。
---
## High — 本次发布前修复
### SEC-002: 默认 JWT / License 密钥随仓库提交,且 release 模式无校验
**文件**`backend/config/config.yaml:10,15``backend/main.go:24-28`
**描述**`config.yaml`(已进 git)内含 `jwt.secret: "change-this-to-a-random-secret-in-production"``license.hmac_secret: "change-this-license-secret-in-production"``main.go` 仅在 release 模式校验 `cors_origin != "*"`**未校验 JWT secret 是否仍为默认值**。生产靠 `production.env``JWT_SECRET` 覆盖,但一旦漏配 env / 配置加载顺序异常,服务会以仓库里公开的默认密钥启动。
**攻击场景**:攻击者用公开的默认 secret 自签任意 `{user_id, shop_id, role:"superadmin"}` 的 JWT`middleware/auth.go:46-48` 只验签名),无 sid 的 token 走「存量放行」分支(`auth.go:56` 注释),直接以任意门店超管身份访问全部接口——完全绕过认证。
**修复建议**:在 `main.go` release 前置检查里追加:`jwt.secret` 为空或等于默认占位串时 `log.Fatal`;同理校验 license hmac_secret。默认值不应是可用值。
**验证方式**release 模式下不设 `JWT_SECRET` 启动,应直接 fatal 退出。
### SEC-003: 运维 workflow 在命令行内联 DB 密码(`mysql -p${DB_PASSWORD}`
**文件**`.gitea/workflows/reset-db.yml:38,45,60,64``.gitea/workflows/debug-db.yml:23-42``.gitea/workflows/seed.yml:44,59`
**描述**:这三个 workflow 把 `${{ secrets.DB_PASSWORD }}``-p${DB_PASSWORD}` 形式内联进通过 SSH 下发到生产/容器执行的 `mysql`/`docker exec` 命令。`debug-db.yml` 用未加引号的 `<< EOF` heredoc,密码在 runner 本地展开后拼进远端命令串。风险:(a) 目标主机 `ps aux` 可见明文密码;(b) mysql 会打印 "password on the command line is insecure"(c) 若任一步骤开 `set -x` 或命令回显,CI 日志泄露密码。对比 `scripts/ci/backup-db.sh:33-34` 已用 `MYSQL_PWD` env 正确处理——说明团队知道正确姿势,只是这三个 workflow 没跟上。
**修复建议**:改为 `docker exec -e MYSQL_PWD="$DB_PASSWORD" jiu_mysql mysql -uroot ...`,去掉 `-p`heredoc 用单引号 `<< 'EOF'` 让密码只在远端环境变量里展开,不进命令行。
**验证方式**:远端 `ps -ef | grep mysql` 执行期间看不到密码;CI 日志无密码串。
---
## Medium — 近期修复
### SEC-004: finance / 库存盘点写入未校验外键归属(SEC-001 写入侧的独立体现)
**文件**`backend/internal/handler/finance.go:81-127``backend/internal/handler/inventory.go:239-271`
**描述**:即便按 SEC-001 修好读取侧 Preload,写入侧仍应独立校验:`finance.Create` 存任意 `partner_id``CreateCheck` 存任意 `product_id`,会在本店数据里留下指向他店实体的脏引用,破坏数据一致性与后续对账。
**修复建议**:所有接收外键的写接口统一做「归属当前 shop」前置校验,抽成公共 helper。
### SEC-005: 公开错误上报端点可伪造身份字段、`error_msg` 无长度上限
**文件**`backend/internal/handler/error_report.go:19-72`、路由 `router.go:78`
**描述**`POST /api/v1/public/errors` 无需认证,`shop_id / username / role / shop_no` 全部取自请求体,攻击者可任意伪造,污染 superadmin 后台的错误列表(`admin/errors`)。`stack_trace` 截断到 4000,但 `error_msg` 未设上限,可提交超大 body 撑存储(仅 IP 限流 30/min 缓解)。
**修复建议**`error_msg` 加长度上限(如 2000);后台展示对这些「客户端自报字段」标注不可信;考虑对 `shop_id` 做存在性校验或改标为 `reported_shop_id`
### SEC-006: 生产库密码经 `python3 re.search` 从 DSN 裸解析,DSN 特殊字符易误解析
**文件**`scripts/ci/backup-db.sh:33`
**描述**`re.match(r"[^:]+:([^@]+)@", d)` 从 DSN 提取密码,若密码含 `@``:` 会截断/错取,导致备份静默失败(虽有 100KB 体积兜底,但错误密码下 dump 会直接失败退出,属可用性风险而非泄露)。当前实现本身不泄露密码(走 `MYSQL_PWD`,正确)。
**修复建议**:改为在远端 source `production.env` 后用 shell 参数解析,或约束 DB 密码字符集;此条为健壮性加固。
---
## Low — 备案,酌情处理
### SEC-007: 兑换码熵与取模偏置
**文件**`backend/internal/util/redeem_code.go:16-31`
**描述**:兑换码 8 位 × 31 字母表 ≈ 40 bit;`int(b)%len(codeAlphabet)`(256 % 31 ≠ 0)引入轻微取模偏置,进一步压缩有效熵。`/license/activate` 需登录且受 per-shop 限流(20rps),在线爆破 40bit 不现实,风险低。
**修复建议**:用 `rand.Int(rand.Reader, big.NewInt(len))` 消除偏置;如担心批量猜码可加每店激活失败限速。
### SEC-008: `product_option` / `product_attr` keyword 搜索 LIKE 通配未转义
**文件**`backend/internal/handler/product_option.go:31,97,163,231`
**描述**`"%"+kw+"%"` 参数化传入(无注入),但用户传入的 `%`/`_` 会被当通配符,可致模糊匹配范围异常(功能性,非安全边界)。同现象遍布各列表搜索。
**修复建议**:如需精确前缀/包含语义,转义 `%``_`
### SEC-009: CORS 允许头未含限流/凭证约束,`*` 仅靠 release 守卫
**文件**`backend/main.go:57-68``config.go:93`
**描述**:默认 `cors_origin: "*"`release 模式已 fatal 拦截 `*``main.go:25-27`),debug 模式放开属预期。当前无 `Allow-Credentials`,风险低。保持现状即可,备案。
### SEC-010: 公开商品接口暴露面
**文件**`backend/internal/handler/public.go``GetProduct`/`ListShopProducts`/`ProductPage`)、`version.go``/health`
**描述**`/health` 仅回 `{"status":"ok"}``/version``/public/release` 实时读 `version.yaml`(无敏感信息)。`public/shops/:shop_code/products` 按 shop_code 列出有库存商品(含 sale_price)——属商品公开展示的设计意图,已加最紧 IP 限流(`shop_list_per_min`)。`ProductPage` 注入的 OG 字段均经 `html.EscapeString``public.go:355,411-418`),无 XSS。暴露面可接受,备案。
---
## 通过检查项
- ✅ 本会话新增代码多租户隔离到位:`summaryBounds`/`rolling30``stockIn/Out.Summary``stock_in.go:191-200`/`stock_out.go:90-99`)、`finance.Trend``finance.go:198-205`)、`inventory.Summary``inventory.go:187-207`)所有聚合均带 `shop_id = ?`
- ✅ 库存/出入库搜索的 `EXISTS/IN` 子查询含 `it.shop_id = ?``stock_in.go:63-68,139-144``stock_out.go:59-65``inventory.go:115-121`);`itemsTable` 为硬编码常量非用户输入(`stock_in.go:87` 注释确认),`series/spec` 多值用 `strings.Repeat("?,")` 生成占位符后参数化,无拼接注入。
- ✅ 所有 LIKE/keyword 均走 GORM `?` 参数化,未见字符串拼接进 SQL。
-`?window` 参数只用 `== "rolling30"` 相等判断,不参与 SQL`stock_in.go:173`);`months``strconv.Atoi` + `1..24` 范围校验(`finance.go:186`)。
- ✅ 新增路由 `/finance/trend``router.go:216`)在 `api.Use(JWT)` + `ReadOnly` + `LicenseGuard` 组内,为 GET 只读;所有写操作路由都在 `ReadOnly()` 之后;`/users``AdminOnly``/admin/*``SuperAdminOnly`
- ✅ 密码 bcrypt 哈希(`service/auth.go:443,496`),`PasswordHash``json:"-"``model/user.go:9`)不外泄。
- ✅ JWT 会话校验支持踢人/禁用即时失效(`middleware/auth.go:56-80`),登录按账号+IP 双重失败锁定、多档 IP/店限流。
- ✅ 库存扣减/单号生成用事务 + `FOR UPDATE` 行锁(`inventory.go:348``service/stock.go` GenerateOrderNo);`admin.ClearData` 用表名白名单 + `shop_id` 过滤(`admin.go:30-51`)。
- ✅ CI secrets 通过 `${{ secrets.* }}` 注入 env,未见 `echo`/`set -x` 打印密钥;`setup_ssh` 私钥写盘后 `teardown_ssh`/`if: always()` 清理(`lib-forgejo.sh:128-130`、各 workflow cleanup 步骤)。`backup-db.sh``MYSQL_PWD` env 传密码(正确)。
-`SetTrustedProxies(127.0.0.1/::1)` + `RemoteIPHeaders=X-Real-IP`,客户端无法伪造限流/审计所依赖的真实 IP(`main.go:54-55`)。
- ✅ 图片上传校验:解码校验真实格式、限 1MB、限每商品 5 张、UUID 文件名、按 productID 归档(`product_image.go:49-96`)。
@@ -0,0 +1,90 @@
# 安全审计报告(发版前增量) — 2026-07-03
**审计范围**:分支 `worktree-front-refactor` 的全部未提交改动(`git diff HEAD` + 未跟踪文件),聚焦**本日首轮审计(`docs/security/2026-07-03-audit.md`Critical/High 已修复)之后**的新增/变更部分:
1. `client/lib/screens/auth/`(登录/注册屏全新重写)+ `client/lib/core/storage/login_history.dart`(记住我)+ `client/lib/repositories/auth_repository.dart`
2. `deploy/nginx-jiu-ali.conf`443 TLS 回切 + 80→443 跳转,8443 拆除)
3. `scripts/ci/*` + `.gitea/workflows/*`EC2→Ali URL/secret 回切)
4. 三端明文豁免(ATS / usesCleartextTraffic)还原核查
5. `backend/` 改动回归扫描(新增 `handler/ownership.go`、多租户 Preload/写入侧校验、`main.go` SEC-002 修复)
**发现问题**Critical 0High 0Medium 2Low 4
**总体结论**:首轮审计的 CriticalSEC-001 跨租户泄露)与 HighSEC-002 默认密钥、SEC-003 CI 内联 DB 密码)在本轮改动中**均已闭环修复且实现正确**。新增的登录/注册屏、nginx 回切、CI 回切**未引入新的 Critical/High**。剩余为纵深加固项(HSTS、盘点写入侧归属校验、商品字典外键归属),可发版后处理。
---
## Critical — 必须立即修复
无。
---
## High — 本次发布前修复
无。
---
## Medium — 近期修复
### SEC-P01: 盘点建单(CreateCheck)写入侧未校验 product_id / warehouse_id 归属
**文件**`backend/internal/handler/inventory.go:239-273``CreateCheck`,无 `ensureShopRef`)、`inventory.go:307-345``CompleteCheck` 盘盈按 `item.ProductID``Inventory` 行)
**描述**:本轮为 stock-in / stock-out / finance 的写接口补齐了 `ensureShopRef`/`ensureShopRefOpt` 外键归属校验(`ownership.go`),但**盘点建单 `CreateCheck` 漏了同款校验**——`req.Items[i].ProductID``req.WarehouseID` 直接透传入库。首轮审计 SEC-001 修复说明第 3 条已点名此处待修,读取侧 `Preload("Items.Product", "shop_id = ?", shopID)` 现已补上(跨店 product 读不出来),但写入侧仍缺。
**攻击/影响场景**:攻击者登录 A 店,`POST /api/v1/inventory/checks` 提交 `items:[{product_id:<B 店的 id>}]`;随后 `CompleteCheck` 走盘盈分支会在 **A 店 inventory 表**新建一行 `product_id=B店id` 的库存记录(`inventory.go:331-343`),snapshot 列因 `item.Product` 被 shop 作用域 Preload 置 nil 而为空。结果:不是直接跨租户读取(读侧已挡),而是**在本店数据里留下指向他店 product 的脏引用**,破坏库存一致性与后续对账/显示兜底。盘亏分支 FIFO 扣减带 `shop_id AND product_id` 双条件,跨店 id 匹配不到批次,无副作用。
**修复建议**:在 `CreateCheck` 复用 `ensureShopRef(h.db, "warehouses", req.WarehouseID, shopID)` 与逐条 `ensureShopRef(h.db, "products", item.ProductID, shopID)`,不符即 400。与 stock-in/out 保持同一道防线。
**验证方式**:A 店 token 提交引用 B 店 product_id 的盘点单,应 400 拒绝,而非建单成功。
### SEC-P02: nginx 443 回切后缺失 HSTS,且 TLS/安全响应头未加固
**文件**`deploy/nginx-jiu-ali.conf:15-23`server 块无 `Strict-Transport-Security` / `ssl_prefer_server_ciphers` / `X-Frame-Options` / `X-Content-Type-Options`
**描述**:8443 明文过渡口刚拆除、回切 443 TLS(`ssl_protocols TLSv1.2 TLSv1.3` 正确,已排除 TLSv1.0/1.1),并新增 80→443 跳转(`:107-112`)。但:
- **无 HSTS**:首次通过 80 访问的用户在被 301 跳转前存在一次明文窗口,可被 SSL-strip 中间人拦截(尤其刚从明文 8443 时代迁移、部分设备可能仍缓存 http 直连习惯)。
- `ssl_ciphers HIGH:!aNULL:!MD5` 较宽松,未配 `ssl_prefer_server_ciphers on`,未启用 OCSP stapling / `ssl_session_cache`,仍允许部分 TLSv1.2 CBC 套件。
-`X-Content-Type-Options: nosniff` / `X-Frame-Options``/app/` 管理端 + 营销站 `location /` 都由此块服务,存在点击劫持/嗅探面)。
**影响**:非直接可利用漏洞,属传输层与浏览器侧纵深加固缺口;在「刚结束明文过渡」的时点尤其值得补 HSTS。
**修复建议**443 server 块加 `add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;`(确认所有 51yanmei 子域都已上 HTTPS 后再考虑 `preload`);补 `ssl_prefer_server_ciphers on;` + 收敛 cipher 列表(Mozilla intermediate);对 `/app/``location /``X-Content-Type-Options nosniff``X-Frame-Options SAMEORIGIN`
**验证方式**`curl -sI https://jiu.51yanmei.com/ | grep -i strict-transport`SSL Labs 评级 A 以上。
---
## Low — 备案,酌情处理
### SEC-P03: 商品字典外键(origin/shelf_life/storage/category/description_doc)无归属校验,读侧 Preload 未按 shop_id 作用域
**文件**`backend/internal/handler/product.go:193-206`Update 透传 `category_id/origin_id/shelf_life_id/storage_id/description_doc_id`,无 `ensureShopRef`)、`product.go:224-225`Detail `Preload("Category").Preload("Origin").Preload("ShelfLife").Preload("Storage")``shop_id` 作用域)、`public.go:40-44,335`(公开页同样无 shop 作用域 Preload
**描述**`ownership.go` 的归属校验模式本轮只覆盖了 partner/warehouse/product**未延伸到商品字典类外键**。用户可把自家 product 的 `origin_id` 等设成他店字典项 id,Detail/公开页会按外键回连读出他店字典的 `Name/Remark`(这些表 = `TenantBase` 门店级)。注意 `product.go` 本轮未改动,属存量残余;且泄露内容为低敏感字典文本(如「酱香型白酒」「阴凉避光保存」),非 PII。
**修复建议**product Create/Update 对上述 5 个外键复用 `ensureShopRefOpt`(对应表 `product_origin_options` / `product_shelf_life_options` / `product_storage_options` / `product_categories` / `product_description_docs`);Detail/public 的 Preload 补 `"shop_id = ?", shopID`(公开页用 product 自身 shop_id)。抽成与 SEC-001 同源的统一收口。
**验证方式**A 店 product 设 B 店 origin_idGET detail 应返回 origin=null 或建单时 400。
### SEC-P04: 「记住我」默认开启,门店编号+登录账号明文留在设备 SharedPreferences
**文件**`client/lib/core/storage/login_history.dart:15,34-47``getRemember` 默认 `true``record` 存 hotel code + username 到 `SharedPreferences`)、`login_screen.dart:52,124-139`
**描述**`_remember` 默认 `true`,登录成功后把门店编号与登录账号(各最多 5 条)明文写入 `SharedPreferences`,下拉历史展示给下一位使用者。**已正确规避:密码从不落盘/不入历史**(仅内存 `TextEditingController``dispose` 释放;无任何 password 日志)——这是本项的关键安全边界,已达标。残余点是共享设备上门店编号/账号可被他人枚举,属账号名泄露(非凭证泄露),且是「记住我」标准取舍。
**修复建议**:可接受现状。如需更严:共享/多用户场景下默认 `false`,或提供「清除登录历史」入口。
**验证方式**:登录后检查 `SharedPreferences`,确认 `login_history_*` 只含 shop code / username,无 password 键。
### SEC-P05: 自助注册字段无长度上限,可提交超大 body
**文件**`backend/internal/service/auth.go:423-431``RegisterInput``binding:"required"` + 密码 `min=6`,各字段无 `max`)、`handler/auth.go:76-90`
**描述**`/api/v1/public/register` 无需认证,`shop_name/address/manager_name/phone/description` 均无长度上限,可提交超大字符串撑存储/日志。已由 per-IP 限流缓解(`register_per_min=5``router.go:79`),且创建时套事务;单次滥用成本有限。同类现象亦见于注册可无限建试用门店(业务滥用,非安全漏洞,限流已缓解)。
**修复建议**:各字段加 `max`(如 name ≤100、address ≤255、phone ≤20、description ≤500);phone 加格式校验。
**验证方式**:提交 100KB 的 shop_name,应 400 而非入库。
### SEC-P06: 443 `default_server` + `server_name "... _"` 使裸 IP/任意 Host 均命中 jiu 块
**文件**`deploy/nginx-jiu-ali.conf:17-18`
**描述**`listen 443 ssl http2 default_server;``server_name jiu.51yanmei.com _;`,意味着任意 Host 头(含 `https://182.92.213.171` 裸 IP)都会命中该块并用 jiu 证书应答(证书 CN 不匹配裸 IP,浏览器告警,但 API/静态仍可达)。这是 default_server 的预期行为、也是「存量客户端域名 BASE_URL 回切即恢复」所需,暴露面与回切前一致,无新增可利用点。
**修复建议**:可接受。如需收紧,可为裸 IP/未知 Host 单设一个 `return 444` 的默认块,仅 jiu 域名走业务块。
**验证方式**`curl -k -H 'Host: x' https://<ip>/health` 行为符合预期即可。
---
## 通过检查项(本轮重点复核)
-**SEC-001 跨租户泄露闭环**:新增 `backend/internal/handler/ownership.go``ensureShopRef`/`ensureShopRefOpt`,table 为调用点硬编码常量、非用户输入)。写入侧已挂:`stock_in.go:235-240,301-306`Create/Update warehouse+partner)、`stock_out.go:206-211,273-278``finance.go:97`Create partner)。读取侧 Preload 全部补 `"shop_id = ?", shopID``stock_in.go:133-136,209-211``stock_out.go:145-149,271-273``finance.go:19`ListRecords)、`finance.go:244`Summary `LEFT JOIN partners ... AND p.shop_id = f.shop_id`)、`inventory.go:233-234,278,293`Logs/GetCheck/CompleteCheck)。partner/warehouse 双向防线完整。
-**SEC-002 默认 JWT 密钥**`main.go:28-33` release 模式新增前置校验,`jwt.secret` 为空或等于仓库默认占位串即 `log.Fatal`,杜绝带公开密钥上线。
-**SEC-003 CI 内联 DB 密码**`debug-db.yml:28-40` / `reset-db.yml:31,59-64` / `seed.yml:43,58` 已全部改为 `docker exec jiu_mysql sh -c 'exec mysql -uroot -p\$MYSQL_ROOT_PASSWORD ...'`——密码从**容器内环境变量**读取,不再以 `-p${DB_PASSWORD}` 出现在下发到远端的命令行,且从 workflow 移除了 `DB_PASSWORD` secret 依赖。远端 `ps` 与 CI 日志均不再暴露密码。`backup-db.sh:31-33` 仍走 `MYSQL_PWD` env(正确)。
-**CI/workflow 回切无 secret 泄漏**EC2→Ali 改用 `ALI_SSH_KEY/ALI_HOST/ALI_USER``backup.yml`/`reset-db.yml`/`seed.yml`/`debug-db.yml`/`manual.yml`),经 `${{ secrets.* }}` 注入 env,无 echo/`set -x` 打印。`manual.yml:31-40` 将旧 `EC2_*` 同值指向 Ali(回滚旧 tag 时不复活 EC2),`setup_ssh` 私钥写盘后 `teardown_ssh`/cleanup 清理。`notify.sh`/`release-client.sh`/`deploy-server.sh`/`local_test.sh` 仅注释/URL 说明变更,全部 `https://jiu.51yanmei.com`,无明文残留。
-**三端明文豁免已还原且无残留**`git diff HEAD``client/android/app/src/main/AndroidManifest.xml``client/ios/Runner/Info.plist` 无改动;全仓 grep `usesCleartextTraffic`/`cleartext`/`network_security_config`/`NSAllowsArbitraryLoads`/`NSExceptionDomains``client/android``client/ios``client/macos` 均**零命中**。前端 `AppConfig` 基址走 `https://` 域名,回切后自动恢复加密传输。
-**nginx 回切暴露面收敛**:8443 明文口拆除(减少一个明文入口);443 `ssl_protocols TLSv1.2 TLSv1.3`(已排除弱协议);未鉴权 `(public|auth)/``/product/``/app/product/` 均挂 `limit_req zone=jiu_pub``X-Real-IP $remote_addr` 透传供后端限流/审计取真实 IP。
-**登录/注册屏无凭证泄露**:密码仅存内存 `TextEditingController``dispose` 释放;`login_history.dart` 只存门店编号+账号,**不存密码**;`api_client.dart:135-139` 的 5xx 上报只发 `[method] path → status`,**不含请求体**(登录失败为 401 不触发上报,即便 5xx 也不带 password);注册成功弹窗仅回显门店编号,无敏感信息。
-**shop_id 全程取自 JWT**:登录/注册屏及后端改动无从请求参数读取 `shop_id` 的路径;所有查询经 `middleware.GetShopID(c)`
-**注册鉴权与限流**`/public/register``registerIP`5/min/IP);`/auth/login``loginIP`10/min/IP);密码入库 bcrypt`service/auth.go:443`)。
-**会话失效提示链路**`login_screen.dart:116-120,334-340` 首帧主动读 + `ref.listen` 双通道捕获 `sessionEndedMessageProvider`,被顶下线有弹窗;消息内容无敏感信息。
-**model 改动为纯 gofmt 对齐**`user_session.go`/`license_device.go`/`license.go` 等仅列对齐空白变更,无语义/字段/tag 改动,多租户索引与 `json:"-"``RefreshJTI`)保持不变。