React Native一次人脸注册引发的 MMKV 数据污染排查复盘

一次人脸注册引发的 MMKV 数据污染排查复盘

日期:2026-08-11

场景:React Native Android 工位机、人脸注册、react-native-mmkv、FaceAI SDK

摘要

项目原本可以正常登录和调用业务接口,但多次执行人脸注册后,陆续出现以下问题:

  • KEY_USER_INFO 无法执行 JSON.parse
  • access token 从正常 JWT 变成短字符串;
  • refresh token 消失;
  • 人脸注册先返回 403,随后变成 401;
  • 401 又触发应用自动登出并删除 token。

最终确认:应用与 FaceAI SDK 同时使用了 MMKV 默认实例 mmkv.default。应用通过 react-native-mmkv 写入登录信息,FaceAI SDK 通过腾讯原生 MMKV 写入人脸特征。两边使用不同版本、不同原生入口操作同一个 mmap 文件,导致应用数据被覆盖或污染。

最终方案是将应用鉴权和设置数据迁移到独立 MMKV 命名空间 wbst-app-storage-v1,彻底与 FaceAI SDK 的默认存储隔离。

一、故障现象

最初日志表现为:

text 复制代码
[FaceAI] Calling arc face registration API...
[FaceAI] Registration failed: Error: 人脸信息注册失败

HeaderBar: Failed to parse KEY_USER_INFO:
JSON Parse error: Unexpected character: R

[FaceAI] Registration failed:
Request failed with status code 401

表面上看,问题可能来自以下任意一层:

  1. Face SDK 没有正确采集图片;
  2. Base64 格式不符合后端要求;
  3. multipart 请求格式错误;
  4. token 过期或登录时保存错误;
  5. MMKV 文件或某些键发生异常。

仅凭这些错误无法判断先后因果,因此需要在关键节点记录相同格式的存储快照。

二、建立可比较的诊断日志

新增统一的鉴权存储摘要日志,记录:

  • 当前服务端 host;
  • token 是否存在、长度、JWT 段数、校验值;
  • refresh token 是否存在;
  • userInfo 是否为合法 JSON;
  • userInfo 长度、校验值和首字符;
  • userId

为避免泄露凭据,日志不打印完整 token,只记录长度、掩码尾号和非加密校验值。

关键埋点包括:

text 复制代码
password-login:after-token-write
password-login:after-user-info-write

face-registration:before-sdk-camera
face-registration:after-sdk-camera
arc-face:before-request

401-interceptor:before-logout-event
app-logout:before-delete
app-logout:after-delete

这种设计的重点不是"多打日志",而是保证每个阶段使用同一种快照结构,从而直接比较数据在哪一步变化。

三、关键证据

1. 登录写入完全正常

登录接口返回的 access token 是标准三段式 JWT:

text 复制代码
token type: Bearer
JWT segments: 3
stored length: 1434
contains undefined/null: false

登录后重新获取用户信息,userInfo 也恢复为合法 JSON:

text 复制代码
jsonValid: true
valueType: object
isAuthenticated: true

因此,登录响应和登录写入不是本次 token 异常的直接原因。

2. 数据变化发生在原生人脸页面返回期间

人脸注册前:

text 复制代码
face-registration:before-sdk-camera
token.length = 1434
token.jwtSegments = 3
refreshToken.exists = true
userInfo.jsonValid = true

第一次从 FaceAI 原生相机页面返回后,token 暂时保持不变,但 userInfo 已消失:

text 复制代码
face-registration:after-sdk-camera
token.length = 1434
userInfo.exists = false

继续重复注册后,某一次相机返回前后发生了决定性变化:

text 复制代码
before-sdk-camera:
  token.length = 1434
  token.jwtSegments = 3
  refreshToken.exists = true

after-sdk-camera:
  token.length = 50
  token.jwtSegments = 1
  refreshToken.exists = false
  userInfo.jsonValid = false

变化发生在调用后端注册接口之前,因此可以排除:

  • Axios 响应拦截器;
  • 401 自动登出;
  • 后端注册接口;
  • React 页面主动清理 token。

3. 401 是结果,不是起因

损坏后的 50 字符字符串被当作 Authorization token 发送:

text 复制代码
Authorization: <非 Bearer、非 JWT 的短字符串>

服务端返回 401 后,项目中的统一拦截器广播登出事件:

text 复制代码
HTTP 401
  -> NETWORK_ERROR_401
  -> setIsSignedIn(false)
  -> 删除 TOKEN 和 REFRESH_TOKEN

因此,"人脸注册后 token 丢了"包含两个连续阶段:

  1. FaceAI 原生流程先污染 token;
  2. 随后的 401 再触发应用主动删除 token。

MMKV 并不是在收到 401 后随机丢失数据。

四、根因定位

应用侧

应用原来使用默认 MMKV 实例:

ts 复制代码
const mmkv = new MMKV();

react-native-mmkv 默认实例 ID 为:

text 复制代码
mmkv.default

项目中的 token、refresh token、用户信息、语言和设备设置都保存在该实例中。

FaceAI SDK 侧

FaceAI React Native 模块源码中直接使用:

kotlin 复制代码
MMKV.defaultMMKV().encode(faceID, faceFeature)
MMKV.defaultMMKV().decodeString(faceID)
MMKV.defaultMMKV().removeValueForKey(faceID)

其 Android 依赖为:

gradle 复制代码
implementation 'com.tencent:mmkv:1.3.14'

当前 react-native-mmkv 包内嵌的 MMKV Core 为 1.3.3。

这意味着应用的 JSI MMKV 与 FaceAI SDK 的 Java MMKV 使用不同原生入口操作同一个默认 mmap 文件。写入人脸特征时,默认存储中的应用键出现丢失、错位或错误解码。

由于 FaceAI SDK 内部 Activity 和依赖也可能直接使用 MMKV.defaultMMKV(),只修改 React Native Bridge 中的几个调用并不能完整隔离风险。

五、最终修复

1. 应用使用独立 MMKV ID

新增统一存储模块:

ts 复制代码
export const APP_STORAGE_ID = 'wbst-app-storage-v1';
export const appStorage = new MMKV({ id: APP_STORAGE_ID });

应用鉴权、用户信息和业务设置全部通过该实例读写。FaceAI SDK 继续使用 mmkv.default,两者对应不同文件,互不覆盖。

相关实现:

  • src/storage/appStorage.ts
  • src/App.tsx
  • src/i18n/MMKVBackend.ts
  • src/utils/AuthStorageDebug.ts

2. 一次性安全迁移

升级已有安装时,需要保留旧环境地址、语言等设置,因此增加一次性迁移:

text 复制代码
旧 mmkv.default
  -> 校验已知应用键
  -> 写入 wbst-app-storage-v1
  -> 写入迁移完成标记

迁移过程中增加额外保护:

  • 非三段式 Bearer JWT 不迁移;
  • 非法 JSON 的 userInfo 不迁移;
  • token 已损坏时不迁移 refresh token;
  • 新存储已有值时不覆盖。

因此,已损坏设备升级后通常需要重新登录一次,但不会把错误鉴权信息带进新存储。

3. 登录写入保护

登录流程不再打印完整密码和 token,仅保留脱敏摘要。

同时,用户信息保存前检查 currentUser.id,避免接口占位实现返回 undefined 后继续读取 res.id,产生未处理的 Promise 异常。

六、人脸注册请求格式的排查结论

Swagger 生成的请求使用:

bash 复制代码
curl -X POST '.../registerFaceFeature' \
  -H 'Authorization: Bearer ***' \
  -H 'Content-Type: multipart/form-data' \
  -F 'UserId=<uuid>' \
  -F 'ImageBase64=data:image/jpg;base64,...'

因此接口要求:

  • multipart/form-data
  • UserId 为文本字段;
  • ImageBase64 为文本字段;
  • Base64 保留完整 data:image/jpg;base64, 前缀;
  • 不上传 Image 二进制文件时,只提交上述两个字段。

客户端最终采用显式 FormData,避免额外封装造成歧义:

ts 复制代码
const formData = new FormData();
formData.append('UserId', String(userId));
formData.append('ImageBase64', String(imageBase64));

403、500、401 的含义不同

状态 本次场景中的含义
403 服务端业务校验失败,例如找不到对应用户
500 请求已进入服务端,但图片处理、特征提取或保存阶段发生未处理异常
401 Authorization 中的 token 无效;本次事故中由 MMKV 污染后的短字符串触发

抓包工具已经能够把 App 请求解析为两个 FORM-DATA 字段,说明 multipart boundary 和字段结构已被服务端接收。500 不等于请求没有进入接口;如果 URL 或媒体类型错误,更常见的是 404 或 415。

不要把 multipart boundary 当成 UserId

multipart 原始内容类似:

text 复制代码
--<随机 boundary>
Content-Disposition: form-data; name="UserId"

<真正的 UserId>
--<随机 boundary>

第一行的随机 UUID 样式字符串只是分隔符,不是字段值。严格比较 App 与 Postman 请求时,应使用 Content-Disposition 下方空行后的值。

七、验证方案

修复后建议连续执行至少 20 次人脸注册,并比较以下日志:

text 复制代码
face-registration:before-sdk-camera
face-registration:after-sdk-camera
arc-face:before-request

通过标准:

  • token checksum 始终一致;
  • token 长度和 JWT 段数不变;
  • refresh token 始终存在;
  • userInfo.jsonValid 始终为 true
  • App 重启后仍能读取同一组数据;
  • Face SDK 可以继续保存和读取本地人脸特征。

请求层还应检查:

text 复制代码
imagePrefix = data:image/jpg;base64,
payloadLengthMod4 = 0
containsWhitespace = false
looksLikeJpeg = true

如果存储快照稳定,但接口仍返回 500,应使用抓包中的完整请求在 Postman 原样重放,并让后端根据同一次请求的追踪信息检查真实异常。此时不应继续修改 MMKV 或 token 逻辑。

八、经验总结

  1. 第三方 SDK 使用默认存储时,必须检查是否与应用共享文件或 key 空间。
  2. 同一个 MMKV mmap 文件不应由两套独立原生封装并行管理。
  3. 鉴权信息应使用独立、明确命名的存储实例,而不是默认实例。
  4. 遇到"数据偶发丢失",应在跨原生页面前后记录同格式快照,而不是只在错误发生后读取。
  5. 401 自动登出会放大上游问题;必须区分"token 先损坏"和"应用随后删除 token"。
  6. 403、500 和 401 分别代表不同执行阶段,不能仅凭提示文字判断请求格式。
  7. 调试日志禁止打印完整密码、access token、refresh token 和完整 Base64 图片。

结论

本次故障不是登录接口随机返回错误 token,也不是 React Native MMKV 无故丢数据。真正原因是 FaceAI SDK 与应用共享 mmkv.default,导致原生人脸注册流程写入特征时污染应用鉴权数据。

将应用数据迁移到独立 MMKV 命名空间后,存储污染链路被切断。后续人脸注册接口出现的 403 或 500,应作为独立的服务端业务问题继续排查,不再与 token 丢失混为一谈。

相关推荐
光影少年3 小时前
React Native网络、存储与原生能力
前端·react native·react.js
光影少年2 天前
react navite原生事件监听、全局事件通知
前端·react native·react.js
大龄秃头程序员4 天前
CocoaPods 静态库 & 动态库实践笔记:从 Podfile 配置到 RN 新架构的一些思考
react native
光影少年5 天前
RN的Fabric 渲染流程
运维·前端·javascript·react native·react.js·fabric
wordbaby6 天前
App 热更新(OTA)原理深解 —— 以 React Native 为例
前端·react native
光影少年10 天前
RN 的EventEmitter 双向通信
前端·react native·react.js
光影少年11 天前
React Native Module 注册流程
前端·react native·react.js
光影少年14 天前
RN Bridge 原理
前端·react native·react.js