← 返回文档导航
状态 历史加固快照(2026-06-25)
范围 beex-service · admin-service · H5 · App
来源 2026-06-24 全工作区安全审计
风险 P0
一句话: 此前 beex-service 整个后端没有用户身份校验——登录发的 access token 从不落库、从不验签,所有接口的「当前用户」直接取客户端自报的 userId(path/param/header)。攻击者枚举 userId 即可读他人钱包、银行卡、身份证、替他人发起提现(IDOR / Broken Authentication)。
本方案: 把 access token 改成签名令牌,加一道鉴权过滤器解析出可信 userId;先以软模式非破坏上线打地基,待「密钥 + 前端带 token + 预演」三前提齐备后切硬模式(默认拒绝 + 控制器只认认证 userId),彻底堵住越权。
阅读边界:本文记录 2026-06-24~25 的鉴权加固背景、提交和迁移路径,用于解释历史决策;它不是当前部署清单。当前接口白名单、鉴权模式和环境变量必须以各仓 main 最新代码、云效密钥组及部署文档为准,不得按本文“待办”直接判断线上仍未完成。
现状(截至本次审计):硬模式默认已开启(SEAHUB_AUTH_ENFORCE 运行时默认 true);此外已上线两项加固:API 限流(RateLimitFilter,IP + X-Beex-Device-Id 两层,超限返回 429 REQUEST_RATE_LIMITED)与单设备登录(新登录挤下线,其它设备下次请求返回 401 AUTH_SESSION_REPLACED)。因此本文以下“待切/待做”状态均为历史快照,均已落地。
1. 问题与目标
| 维度 | 修复前(现状) | 修复后(目标) |
| access token | Ids.newId("sat") 随机串,不落库、不验签 | HMAC-SHA256 签名令牌,载荷含 userId/会话/国别/过期 |
| 当前用户从哪来 | 客户端自报 userId(path/param/header) | 过滤器验签后注入的 AuthContext.userId |
| /api/v1/** 默认策略 | 全部放行(无任何鉴权过滤链) | 默认拒绝 + 公开端点白名单 |
| 越权(IDOR) | 可枚举 userId 读他人钱包/银行卡/身份证、替他人提现 | 只能操作令牌所属本人数据 |
2. 方案总览(流程图)
flowchart TD
L["用户登录
(WhatsApp/Google/TikTok/Apple/手机/密码)"] --> T["TokenService.issue
用 SECRET 签发签名 access token"]
T --> C{"客户端类型"}
C -->|"App 内 H5"| BR["Flutter nativeRequest 桥
注入 Bearer(仅 BeeX 自有域)"]
C -->|"纯浏览器 H5"| AX["axios 请求拦截器
从 localStorage 注入 Bearer"]
BR --> F["UserAuthFilter
读 Authorization → 验签"]
AX --> F
F -->|"验签通过"| CTX["AuthContext = 认证 userId"]
F -->|"无/无效 token"| SOFT{"软模式? "}
SOFT -->|"软模式:放行(回退老逻辑)"| H[("控制器")]
SOFT -->|"硬模式:401 拒绝
(除公开白名单)"| X["拒绝"]
CTX --> H
H -->|"硬模式"| U["以 AuthContext.userId 为准
忽略客户端自报 userId"]
为什么用签名令牌而不是「token 落库 + 每请求回查」: 免去各国库 schema 迁移、每请求零额外 DB 往返(利于扩容);且后端此前从不校验 token、客户端不解析 token,切换令牌格式零破坏。撤销靠 1 小时短有效期 + 登出/封禁时撤销 refresh token/会话兜底。
3. 软模式 vs 硬模式
| 模式 | 行为 | 影响 | 状态 |
| 软模式 |
带有效 token 才把认证用户填进 AuthContext;无/无效 token 也照常放行,后端走老逻辑。 |
非破坏。线上行为不变,纯铺地基。 |
✅ 已上线 |
| 硬模式 |
/api/v1/** 业务请求没有效 token 直接 401;控制器只认 token 里的 userId、忽略客户端自报。 |
真正堵 IDOR。但一刀切影响所有请求,必须满足三前提后才切。 |
🟡 待切(见 §4) |
4. 切硬模式的三个前提
硬模式是「开总闸」,一拍下去影响所有用户的所有请求。三个前提缺一不可,否则会把合法用户挡在门外:
① 配密钥 🟡 待运维
- 所有实例配同一个
SEAHUB_AUTH_ACCESS_TOKEN_SECRET。
- 没配 → 进程级随机临时密钥:2 台 api 互不认、重启即全员登出。
- 详见 §5。
② 前端带 token ✅ 代码完成
- App 内由 Flutter 桥注入(已加自有域白名单)。
- 纯浏览器 H5 由 axios 拦截器注入(此前不带,只靠 userId 参数)。
- 待发版:H5 build+OSS、App 出包。
③ 预演验证 🟡 待做
- 测试/预演环境先开硬模式,跑全链路。
- 确认:该放行的(登录/回调/健康检查)在白名单、该拦的越权确实 401、新老 App 版本都正常。
- 通过 → 灰度 → 全量。日期〔TODO〕。
铁律: ① 三前提全齐才切硬模式,顺序是「前端先发版 → 预演通过 → 后端切」。② 切换是受控变更,不在生产直接拍开关。③ 控制器迁移到 AuthContext.currentUserId() 时,资金接口(钱包/提现/支付)优先,且必须忽略客户端传入的 userId。
公开端点白名单(硬模式必放行)
硬模式默认拒绝 /api/v1/**,但以下端点必须放行——尤其 登录接口本身不能鉴权(登录前根本没有 token)。下表按代码实际路径枚举(标 〔需逐个确认〕 的须上线前逐个核对是否真无用户态):
| 类别 | 路径 | 为何必须公开 |
| 登录 / 认证 | /api/v1/auth/**(options、whatsapp login-intents、google id-token、tiktok/apple、phone/password、password-reset) | 登录前没有 token,鉴权会让人永远登不进来 |
| OAuth 回调 | /api/v1/auth/tiktok/callback、/api/v1/auth/apple/callback、/api/v1/integrations/tiktok/** | 第三方平台回跳,无用户登录态 |
| 第三方 webhook | /webhooks/**(xendit、whatsapp、feishu/events、apple) | 由各自签名/校验 token 验证,不是用户 Bearer |
| 健康检查 | /actuator/health | ALB 健康探测,匿名访问 |
| 登录前公开数据 | /api/v1/app/**(runtime-config、h5-package 版本)、/api/v1/popular-brands 〔需逐个确认〕 | App 启动 / 版本检查 / 首页,未登录可读;但 /api/v1/app/star 等可能带用户态,须逐个确认 |
| 账号注销意图 | /api/v1/account-deletion/intents/** 〔需逐个确认〕 | WhatsApp 管理员确认流程部分匿名,须确认哪些步骤可公开 |
铁律: 白名单只能明确列举,凡未列入的 /api/v1/** 一律拒绝(default-deny);登录、OAuth 回调、webhook、健康检查必须在内。〔需逐个确认〕 的端点上线前逐个核对,预演的首要目标就是抓出漏放行的合法端点(否则用户登录/支付回调会被误杀 401)。
5. 密钥 SEAHUB_AUTH_ACCESS_TOKEN_SECRET 详解
它是什么: 给登录令牌签名 / 验签用的 HMAC 密钥——像一枚只有服务端有的火漆印章。登录时服务端用它给 token(内含 userId 等)盖签名;请求来时用同一把密钥重算签名比对,一致才说明 token 是自己签发、未被篡改,里面的 userId 才可信。别人不知道密钥就伪造不出有效签名,这正是堵 IDOR 的根。
| 问题 | 说明 |
| 不配会怎样 | fallback 成每进程随机临时密钥:生产 2 台 api 实例各一把 → 在 A 登录的 token 到 B 验不过 → 硬模式下随机 401;重启密钥变 → 全员登出。单机/开发可凑合,生产多实例必须配。 |
| 怎么生成 | 高强度随机串(64 位十六进制):见下方命令。 |
| 怎么配 | 设成环境变量,同一环境所有实例同值(api×2、wa、worker 等凡签发/校验 token 的都要)。 |
| 环境隔离 | id-test 与 id-prod 用不同密钥,避免测试 token 在生产可用。 |
| 保密级别 | 当机密对待:谁拿到都能伪造任意用户 token=任意账号接管,等同数据库密码。不入库,走部署 env / 密钥管理注入。 |
| 轮换 | 更换会让所有现存 token 失效(全员重新登录/刷新),挑低峰、提前知会。 |
# 用辅助脚本生成 + 填写指引(推荐)
ci/gen-auth-secret.sh
# 或手动生成
openssl rand -hex 32
多机怎么填(推荐): 把生成的值粘进云效流水线「加密变量」 SEAHUB_AUTH_ACCESS_TOKEN_SECRET(每环境一个)。
部署时 ci/deploy-service.sh 会自动写进每台 .env,全机同值、不进 git、轮换只改云效一处。
单机/手动: 在目标机 ci/gen-auth-secret.sh --set <同一个值> 写入 .env 后 systemctl restart;--show 可脱敏查看现值。
代码侧已就绪:application.properties 里 seahub.auth.access-token-secret=${SEAHUB_AUTH_ACCESS_TOKEN_SECRET:},留空才走临时密钥。配上即生效,无需改代码。
6. 实施进展
截至 2026-06-25,以下变更已编译通过并推送各仓 main:
| 提交 | 仓库 | 内容 | 对应 P0 | 状态 |
| 093266f | beex-service | 生产启用 MOCK 支付即 fail-fast(启动守卫) | 凭空充值提现 | ✅ 完成 |
| 1a1d4ea | beex-service | 签名 access token + 软模式 UserAuthFilter + AuthContext | 无用户鉴权(地基) | 🔵 地基已上 |
| b91b0e8 | beex-admin-service | /api/v1/** 默认拒绝 + h5-packages 可选流水线 token | 匿名改佣金配置 | ✅ 完成 |
| d6af185 | beex-app-h5 | 浏览器路径 axios 注入 Bearer | 前端就绪 | 🔵 待发版 |
| c781b98 | beex-app | nativeRequest 桥仅对 BeeX 自有域注入 token | 前端就绪 + 令牌泄漏 P2 | 🔵 待出包 |
7. 待办与分工
| 事项 | 负责 | 状态 |
配 SEAHUB_AUTH_ACCESS_TOKEN_SECRET:ci/gen-auth-secret.sh 生成 → 填云效加密变量(每环境一个),部署自动写入各机 .env(各实例一致、各环境不同) | 运维 | 🟡 待做(脚本/注入已就绪) |
| H5 发版(build + OSS/CDN)、App 出包,让 token 真正带上 | 前端 / App | 🟡 待做 |
控制器迁移到 AuthContext.currentUserId()(钱包/提现/支付优先) | 后端 | 🟡 待做 |
| 实现硬模式开关(默认拒绝 + 公开白名单) | 后端 | 🟡 待做〔开关名 TODO〕 |
| 预演:测试环境开硬模式跑全链路回归 | 后端 + 测试 | 🟡 待做 |
过渡止血:api/admin-api 安全组只放行办公室 IP/VPN | 运维 | 🟡 待做 |
| P0 #7:静态站点测试域名外置 + payout-profile PII 脱敏策略 | 后端 / 前端 | 🟡 待定方案 |
8. ⚙️ 本方案可配参数
| 参数 | 初始值 | 配置位置 | 状态 |
| SEAHUB_AUTH_ACCESS_TOKEN_SECRET | 空 →(进程级临时密钥) | 云效加密变量注入 → 各机 .env(deploy-service.sh 自动写入);各实例一致、各环境不同 | 🟡 待运维填值 |
| SEAHUB_AUTH_ACCESS_TOKEN_TTL_SECONDS | 3600 | application.properties | ✅ 默认即可 |
| SEAHUB_PAYMENT_PROVIDER | 代码兜底为 MOCK;生产部署基线必须为 XENDIT | 云效密钥组 / 部署 env | 生产守卫已实现 |
| SEAHUB_PAYMENT_MOCK_ENABLED | 代码兜底为 true;测试与生产部署必须显式 false,只有本地隔离测试可开启 | 云效密钥组 / 部署 env | 生产守卫已实现 |
| seahub.admin-api-auth.h5-package-register-token | 空(兼容放行) | admin 部署 env | 🟡 可选加固 |
| 硬模式开关 | 软模式(不拒绝) | 〔TODO 待定开关名〕 | 🟡 待预演后切 |