← 返回文档导航

用户鉴权加固方案

签名式 access token · 软/硬模式渐进切换 · 堵住「客户端自报 userId」越权(IDOR)
状态 历史加固快照(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. 问题与目标 · 2. 方案总览(流程图) · 3. 软模式 vs 硬模式 · 4. 切硬模式的三个前提 · · 公开端点白名单 · 5. 密钥 SECRET 详解 · 6. 实施进展 · 7. 待办与分工 · 8. 可配参数

1. 问题与目标

维度修复前(现状)修复后(目标)
access tokenIds.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/healthALB 健康探测,匿名访问
登录前公开数据/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-testid-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 <同一个值> 写入 .envsystemctl restart;--show 可脱敏查看现值。
代码侧已就绪:application.propertiesseahub.auth.access-token-secret=${SEAHUB_AUTH_ACCESS_TOKEN_SECRET:},留空才走临时密钥。配上即生效,无需改代码。

6. 实施进展

截至 2026-06-25,以下变更已编译通过并推送各仓 main:

提交仓库内容对应 P0状态
093266fbeex-service生产启用 MOCK 支付即 fail-fast(启动守卫)凭空充值提现✅ 完成
1a1d4eabeex-service签名 access token + 软模式 UserAuthFilter + AuthContext无用户鉴权(地基)🔵 地基已上
b91b0e8beex-admin-service/api/v1/** 默认拒绝 + h5-packages 可选流水线 token匿名改佣金配置✅ 完成
d6af185beex-app-h5浏览器路径 axios 注入 Bearer前端就绪🔵 待发版
c781b98beex-appnativeRequest 桥仅对 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_SECONDS3600application.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 待定开关名〕🟡 待预演后切