ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

React Native for OpenHarmony 商城账号安全实践:凭证分层与HUKS

React Native for OpenHarmony 商城账号安全实践:凭证分层与HUKS 先交代一下项目背景。我这个项目的代号就叫 rn_for_openharmony不是个演示壳子而是一套基于 React Native for OpenHarmony 开发的商城 App业务覆盖注册、登录、商品列表、购物车、下单、地址管理、订单查询这些电商全流程。项目做到中后期我把账号安全这一块整个推翻重做了一遍——准确说是把原本“登录成功后把 token 塞进本地存储”的那套做法全部换掉。这篇就来完整记录这次重做的思路、选型、落地代码以及上线前后踩过的那些坑。如果你正在用 rn_for_openharmony 或者类似的跨端方案搞商城类 App这篇尤其值得看完。商城账号背后是手机号、收货地址、订单记录、支付通道一旦账号体系出事前面商品页做得再好都等于白做。账号安全不是“登录页加个验证码”就完事它是一个贯穿全生命周期、需要前后端和原生能力协同的工程问题。1. 先说清楚rn_for_openharmony 商城项目的账号安全威胁范围究竟在哪1.1 项目背景这不是演示 Demo是一套真跑业务的电商 Apprn_for_openharmony 这个项目的技术路线很简单UI 和大部分业务逻辑用 React Native 写TypeScript 为主凡是涉及系统安全能力和设备底层的部分通过原生模块桥接到 OpenHarmony 系统接口。这样做的原因不多说跨端复用是我们选型的第一理由。但正因为是跨端很多人会不自觉地默认“React Native 在安卓上怎么用在 OpenHarmony 上也怎么用”这个想法在账号安全上非常坑。OpenHarmony 的应用沙箱、权限模型、密钥管理接口跟安卓不是一回事连“把字符串存到本地”这种看起来人畜无害的操作在不同平台上都有不同的安全边界。我在这个项目里最深刻的一个体会是跨端框架只帮你跨 UI不帮你跨安全。业务层可以轻松复用但凭证保护的每一层都得重新核对一遍底层能力。这套商城 App 跑在 OpenHarmony 类设备上面向的是真实用户所以它的账号安全至少要满足几个硬性条件用户密码不能被泄露、登录态不能被随意窃取、短信验证码接口不能被刷爆、账号在异常设备上登录时系统要能察觉以及整个 App 不能被反编译之后轻松改造成重打包应用。1.2 电商账号的真实威胁列表先列一份我在设计之初整理出来的威胁清单。这个清单不是从网上抄的是结合商城业务一条条推出来的威胁类型常见发生路径账号安全影响主要应对方向撞库与弱口令用户在别处泄露密码后同一套账号密码被脚本批量尝试账号被批量接管服务端限流、风控、验证码门槛短信验证码绕过验证码接口被刷、验证码明文返回、无频率限制注册/登录被机器人占领人机校验前置、服务端次数控制登录 token 被窃取token 明文存入本地文件或通过日志、备份被导出登录态被冒用token 加密存储、短期有效期中间人抓包使用 HTTP、无证书校验或忽略证书异常请求参数与 token 明文泄露HTTPS 全链路、证书固定客户端重打包反编译后植入恶意代码再签名分发账号凭证和用户数据被盗签名校验、完整性上报设备丢失/换机手机遗失后本地凭证未及时清理账号被直接使用生物识别解锁、远程撤销刷新令牌会话固定/会话劫持登录态未绑定设备信息凭证被复制到其他设备设备指纹、token 与设备绑定这个表格做出来后我已经意识到账号安全不是某一个点而是分层防御。单靠一个“强密码策略”解决不了 token 被本地窃取的问题单靠 HTTPS 也解决不了验证码接口被刷的问题。每一层都要有专门的处理方案。1.3 为什么不能照搬 Android 的安全实践踩过几次坑之后我把它总结成一个观点OpenHarmony 的安全边界跟安卓有很大差异照搬只会做出“看起来安全实际一捅就破”的方案。在安卓上我们习惯用 KeyStore、EncryptedSharedPreferences、安全网络配置这套组合在 OpenHarmony 上对应的系统能力是通用密钥库HUKS、安全存储Asset这类接口命名和职责虽然相似但调用方式、密钥别名管理、权限声明都是另一套写法。项目初期我因为不熟悉直接把安卓的“AES 密钥写死在代码里然后加密本地数据”这套搬过来结果第一个版本就被安全测试打回来了——密钥写死在 JS bundle 里反编译之后等于没有加密。另一个很容易踩的坑是中间人。OpenHarmony 上很多设备默认允许用户把抓包工具的 CA 证书装进“用户证书”区如果你的 App 没有做证书固定那么在测试环境用 Charles 一抓就是一片明文。安卓那边大家已经习惯用 network_security_config 控制证书信任范围OpenHarmony 这套机制的名字和生效条件不同不提前核对上线就等着被公开漏洞报告砸脸。所以我的结论是先理解威胁模型再选 OpenHarmony 的系统能力然后通过原生模块暴露给 RN 层。顺序不能反。2. 方案选型凭证分层、安全存储与原生桥接的取舍2.1 三层凭证设计短期访问令牌、长期刷新令牌、匿名设备标识商城 App 的登录态不能简单用“一个 token 存天下”。我最终采用的凭证模型分三层凭证有效期存储位置用途Access Token30 分钟内存 加密偏好存储每次 API 请求的鉴权凭证Refresh Token7 天可续期系统安全存储HUKS/Asset换取新的 Access Token支持退出与撤销Device Anonymous ID长期加密偏好存储风控身份标识不直接关联用户手机号Access Token 做短期是因为它暴露面最大每次请求都要带一旦被日志、抓包、内存 dump 搞出去至少要控制损失范围。Refresh Token 是长期凭证所以它必须放在系统安全存储里而且服务端要做撤销和轮换每次刷新后旧的 Refresh Token 作废防刷与防重放都靠这个机制。设备匿名标识是商城风控的“锚点”。同一账号在陌生设备上登录后端靠的就是这个 ID 加设备信息做相似度判断。它不能是手机号本身也不能是用户能随意改的业务字段。选型逻辑很简单凭证越值钱存放越深凭证越常用有效期越短。这套模型前后端是同步设计的不是客户端单方面决定。2.2 数据分级存储什么数据放哪里一定要有明确规矩在 RN 项目里最容易出现的偷懒是所有状态都扔进 AsyncStorage所有缓存都扔进本地数据库。我在项目里定了一个数据分级表后端同学和前端的我都必须遵守A 级最高敏感Refresh Token、支付密钥、用户支付凭证。只能进 OpenHarmony 的 HUKS 或安全存储并且要用系统生成的随机密钥加密禁止硬编码密钥。B 级敏感Access Token、设备匿名 ID、最近一次登录的手机号脱敏缓存。进加密后的偏好存储启动时读取到内存页面切换不落盘。C 级普通商品列表缓存、图片缓存、埋点事件队列。可以放在普通偏好存储或 SQLite丢了不影响安全。实际执行中的一条铁律Refresh Token 绝不能进 AsyncStorageAccess Token 也绝不能写进日志系统。我见过很多跨端项目把 token 直接打到 console 里方便调试结果 release 包忘关日志输出到系统日志里被其他应用读到。这个坑一定要从一开始就用代码规范堵死。2.3 为什么要把安全能力封装成原生模块给 JS 层调用React Native 的 JS 层没有一个现成的、跨版本稳定的“钥匙串”接口尤其 OpenHarmony 生态里不同发行版对 HUKS 的实现还有细微差异。所以最稳妥的做法是写一个原生模块叫 SecureStorageModule只暴露几下几个方法给 JS 层saveSecret(alias: string, value: string): PromisevoidgetSecret(alias: string): PromisestringdeleteSecret(alias: string): PromisevoidisDeviceSecure(): Promiseboolean这样做的收益很明显第一密钥管理、加解密算法、密钥生命周期全部收敛到原生侧JS 层不需要关心底层实现第二后续如果 OpenHarmony 的 HUKS 接口升级只需要改原生模块不需要牵动整个业务层第三安全审计时可以只盯着这一个模块范围小好检查。代价也很直接原生桥接的调试成本稍微高一点尤其是 ArkTS 侧的数据同步问题JS 层是 Promise 异步原生侧如果写成同步返回会出现异常。这块我建议从一开始就统一用异步回调风格别混用。3. 核心落地从注册登录到登录态保鲜的完整实现3.1 注册/登录接口客户端的参数处理与服务端配合商城账号体系我采用的是“手机号 短信验证码”这套主流方案但客户端这边的处理并不是简单地“输入手机号点发送验证码”。验证码发送接口必须先过图形验证码/行为验证码。如果不加这个门槛短信接口会被脚本刷到欠费。我在商城项目里把行为验证码放在真正调发送短信之前用户先完成滑块拼图或者点选验证拿到一个一次性的票据再带上票据请求发送短信。这个票据后端会校验时效性和一次性。登录请求的构造也不只是把手机号和验证码拼一起 POST 出去。客户端至少要带这几项deviceId设备匿名 ID、deviceInfo设备型号/系统版本/分辨率、appVersion、timestamp并且对整个请求做签名。服务端验签之后再去走正式的登录逻辑。// 登录请求参数构造TypeScript 层 const loginParams { phone: phoneNumber, smsCode: smsCode, deviceId: await deviceFingerprint.getId(), deviceInfo: await deviceFingerprint.getDeviceInfo(), appVersion: 1.2.0, timestamp: Date.now(), }; // 请求签名由统一的 request 层完成 // 签名规则secret HMAC_SHA256(请求体 timestamp, signingKey) const signedRequest signRequest(loginParams);这里有个容易被忽略的点签名密钥不能被硬编码在 JS bundle 里。我第一个版本就是图省事把签名密钥直接写死在常量里结果反编译一次就全部暴露。正确的做法是签名密钥由服务端通过 HTTPS 下发或者走 HUKS 动态生成的密钥对客户端只负责保存和使用不落明文。3.2 token 拉取与刷新链路别让 401 把 APP 刷崩溃登录成功之后后端会返回 Access Token、Refresh Token 以及 token 有效期。客户端的处理链路是这样Access Token 存内存里同时用加密偏好存储落一份防止 App 进程被杀后冷启动没内存态。Refresh Token 通过SecureStorageModule存进 HUKS。每次请求前封装的AuthRepository判断当前 Access Token 是否临近过期如果剩余时间小于 5 分钟先刷新再发请求。如果请求过程中收到 401则进入“刷新队列”多个并发请求只触发一次刷新。下面这段代码是刷新队列的核心逻辑。我把它放出来因为很多 RN 项目就是没处理并发刷新导致 30 个 401 同时触发 30 次刷新请求服务端直接以为是攻击把账号临时冻结了class AuthRepository { private refreshPromise: Promisestring | null null; async getValidAccessToken(): Promisestring { const current await this.getAccessToken(); if (current !this.isExpiredSoon(current)) { return current; } return this.refreshTokenAndCache(); } private refreshTokenAndCache(): Promisestring { if (this.refreshPromise) { return this.refreshPromise; } this.refreshPromise SecureStorageModule.getSecret(refresh_token) .then(async (refreshToken) { const res await api.refreshToken(refreshToken); // 服务端按约定做 refresh token 轮换 await this.cacheTokens(res.accessToken, res.refreshToken); return res.accessToken; }) .finally(() { this.refreshPromise null; }); return this.refreshPromise; } }这里还需要一个细节所有请求模块都必须经过同一个拦截器403 或者 Refresh Token 已被撤销的错误码要统一触发“强制登出 引导用户重新登录”不能在业务代码里到处 catch 401 然后乱跳页面。我在项目里专门维护了一张“登录态失效错误码表”一旦命中全局弹窗提示“登录已过期请重新登录”同时清理本地凭证。3.3 用 OpenHarmony HUKS 保护 Refresh Token原生模块怎么调这一节是直接和系统能力打交道的部分。OpenHarmony 上保存 Refresh Token我最终走的是 HUKS 生成 AES-GCM 密钥用这个密钥加密 token 后再落盘。落盘的数据本身就算被导出没有 HUKS 里的密钥也无法解密。ArkTS 侧的原生封装大概是这个流程import huks from ohos.security.huks; const alias shop_refresh_token_v1; // 生成 AES-256-GCM 密钥伪代码真实参数以你当前 SDK 为准 const genParams { algorithm: huks.HuksAlgorithm.HUKS_ALG_AES, keySize: huks.HuksKeySize.HUKS_AES_KEY_SIZE_256, purpose: huks.HuksKeyPurpose.HUKS_KEY_PURPOSE_ENCRYPT | huks.HuksKeyPurpose.HUKS_KEY_PURPOSE_DECRYPT, padding: huks.HuksKeyPadding.HUKS_PADDING_NONE, }; await huks.generateKeyItem(alias, genParams); // 加密 const cipher new huks.HuksCipher(); const encrypted await cipher.encrypt(alias, plainToken); // 落盘 await preferences.put(encrypted_refresh_token, encrypted);用的时候要注意一点HUKS 生成的密钥跟应用沙箱目录绑定卸载 App 后密钥会消失本地加密的 Refresh Token 也就解不开这是符合预期的行为反而保证了卸载重装之后旧凭证自动失效。所以业务流程上要给用户一个感受“卸载重装后需要重新登录”是正常的不能让用户以为出了 bug。RN 侧调用就很简单// 保存 refresh token await SecureStorageModule.saveSecret(refresh_token, refreshToken); // 读取 refresh token const token await SecureStorageModule.getSecret(refresh_token);我要强调一个原则HUKS 加密保护的是存储态不是运行态。Refresh Token 一旦被读出来走刷新接口它就在内存里存在一段时间。所以我们只把它暴露给 AuthRepository 这一个类任何页面组件都不允许直接读更不能放进全局状态管理里到处传。3.4 退登、切账号与截图录屏防护的细节账号安全的最后一个易漏点是退出登录和多账号切换。退出登录我分成两种普通退出和安全退出。普通退出只清 Access Token 和内存态Refresh Token 仍然保留设计上是为了“不小心点退出还可以免密登录”安全退出则是全部清掉Refresh Token 同时通知服务端撤销。涉及到钱的操作改密码、换绑手机、支付密码设置客户端要开启隐私保护模式。OpenHarmony 上对应有窗口隐私模式 / 禁止截屏录屏的相关接口在进入这些页面时强制开启离开时关闭。这个之前没注意上线后才发现用户在支付页面截屏留档操作太随意给账号安全埋了不小的隐患。4. 账号安全的二次验证验证码、设备指纹与风控埋点4.1 验证码链路短信接口防刷比想象中难一点短信验证码是被攻击最频繁的一层。我前面提到过行为验证码前置再补充两个关键点第一客户端拿到的短信验证码永远不能展示在日志或前端任何地方。有一个规律是短信验证码在下发之后会被运营商通道或者 App 日志打印出来我见过一个项目把验证码实体对象整体打印出来直接等于把门槛拆了。第二服务端的频率限制要细化同一手机号 60 秒内不能重复发送、同 IP 每小时发送上限、同设备 ID 每日发送上限。单靠客户端限制毫无意义因为脚本根本不走你的 App。正确做法是服务端统一限流客户端只做体验层的克制比如按钮倒计时。4.2 轻量设备指纹与异地登录提醒设备指纹是风控里很关键但又容易被做歪的部分。我的原则很简单只做合规范围内的设备识别不碰通讯录、短信、相册权限。设备匿名 ID 的生成逻辑是首次启动时用随机 UUID 设备型号 系统版本哈希组合生成存入加密偏好存储之后永远不变每次登录、下单时把 Device ID 和几个基本设备特征上报。后端拿到这些信息后可以做“常用设备”判断。比如用户一直用同一台设备登录商城某天突然换了一台设备同时 IP 也从另一个城市冒出来这时候就要触发二次验证要求短信验证或者要求回答最近订单里的某个商品而不是直接放行。这个机制给商城省下了一大堆盗号投诉。4.3 后端风控真正在意的埋点以及哪些别乱上报做客户端埋点的时候我一开始只会上报“用户登录失败了几次”后来跟安全组对齐才发现真正有价值的埋点比这细得多行为序列从启动到下单的路径、每一步的耗时这个对机器人识别很有用。环境变化同一会话内 IP 段跳变、设备型号突然改变、系统语言发生切换。App 完整性客户端计算的包签名哈希和代码哈希上报给服务端用于发现重打包。同时也有几类数据不能上报通讯录内容、短信内容、精确到街道的 GPS 坐标、剪切板内容。这些属于用户隐私收集即有合规风险。我当时直接定了一条红线风控埋点只收“设备侧特征和行为指标”不收“用户内容”。这条红线到现在都没破。5. 上线前安全测试抓包、XTS 认证与多端真机踩坑实录5.1 第一次抓包测试就暴露的问题上线前我用 Charles 模拟抓包问题立刻显现登录请求竟然能抓到全部参数虽然用的是 HTTPS但 CA 证书被装进系统信任区后流量直接暴露。这个阶段的教训很典型只在逻辑代码层面做安全没在网络传输层做防护等于在透明玻璃房里锁保险箱。对策分两步。第一步全链路强制 HTTPS并且配置只信任服务端下发的固定证书指纹也就是证书 Pinning。React Native 层的网络库要支持配置证书固定我直接把网络模块也封装成了原生模块因为 JS 层配置证书固定的方式在 OpenHarmony 上不可靠。第二步测试环境用一个开关允许 CA 证书做调试但 release 构建强制关闭并用构建时校验保证这个开关不会误开到生产包。5.2 XTS 认证里和安全相关的几个检查点项目做 XTS 认证适配的时候和安全相关的重点集中在三类场景检查项最容易挂的地方我的处理敏感数据存储本地偏好存储里出现明文 token全部改成 HUKS 加密存储禁止明文权限最小化申请了非必要的系统权限只保留网络、存储、相机等业务必需权限应用签名与完整性打包后被二次签名未检测启动时读取签名哈希与服务端下发的白名单比对XTS 不是虚构的合规税它确实能逼着你把脆弱点暴露出来。我第一次跑就发现一个问题无意识地把设备匿名 ID 和用户缓存放在同一个配置目录权限设置过于宽松导致其他同签名应用可能读到。拆开之后这个问题自然消失。5.3 多设备真机回归HUKS 行为差异和生命周期边界OpenHarmony 不是一个单一设备形态真机上不同厂商适配层的 HUKS 行为、生物识别接口调用结果都可能不一样。回归测试时我至少要在三类设备上过一遍标准 OpenHarmony 开发板/模拟器验证基本功能和接口兼容。开源手机/平板类真机验证锁屏生物识别、后台清理、权限弹窗交互。定制版设备验证厂商对安全存储的特殊实现是否有差异。除了设备差异进程生命周期边界也要测。例如用户杀进程之后重新打开 AppAccess Token 内存态没了要从加密偏好存储恢复Refresh Token 过期之后从 HUKS 删除且不能留下残留凭证在本地。这些边界我一开始没测全后来出现“用户明明点了退出杀进程重开竟然还是登录态”的问题很尴尬。6. 项目上线后我在账号安全这件事上的几点体会6.1 安全强度与用户体验的平衡账号安全做过头用户流失会非常快。我在商城项目里试过最严格的策略每次进入支付页都要求重新输入登录密码或图形验证码结果支付转化率明显下降。后来改成分级认证普通浏览和低风险操作不打扰只有改密、换绑、大额订单、异地登录这些高风险动作才启动二次验证效果好了很多。安全功能的最终标准不是“越难越好”而是“在关键节点上没有明显的可利用缝隙”。我现在的原则是常规操作能够防住七成攻击风险操作能防住九成五关键操作必须有第二因素。6.2 如果重新做一遍我会更早注意什么如果现在让我从零开始再走一遍 rn_for_openharmony 商城项目的账号安全我会把下面这几件事排到最前面第一周就确定凭证分层和存储分级方案而不是等业务上线后再返工。第一版网络层就做证书固定和请求签名后面补这个比一开始做要痛苦十倍。每一个登录态相关方法都写清楚“它在哪个模块被调用、能不能被页面直接接触”用一个小的架构约束来防止以后有人把 token 写到全局状态里。在 OpenHarmony 这种相对新一些的生态里做跨端商城账号安全没有现成的银弹能依靠的就是一层层把上面说的这些边界补齐。rn_for_openharmony 项目这轮重做之后再回头看最值钱的反而不是某段代码而是“每一层凭证都存在合适的位置每一个敏感操作都有对应的验证手段”这个整体框架。多花点时间在这个框架上比急着堆功能来得长远。
返回列表