跳至主内容
版本:0.79

安全性

非官方测试版翻译

本页面由 PageTurner AI 翻译(测试版)。未经项目官方认可。 发现错误? 报告问题 →

开发应用时,安全性常常被忽视。确实,构建完全无懈可击的软件是不可能的——毕竟我们尚未发明绝对无法攻破的锁(银行金库也仍会被攻破)。然而,遭受恶意攻击或暴露安全漏洞的概率,与你为保护应用所付出的努力成反比。普通挂锁虽然能被撬开,但比起橱柜挂钩仍难突破得多!

本指南将介绍存储敏感信息的最佳实践、身份验证、网络安全以及加固应用的工具。这并非一份预检清单,而是可选方案的目录,每个方案都能为你的应用和用户提供更深层的保护。

存储敏感信息

切勿将敏感 API 密钥存储在应用代码中。任何包含在代码中的内容都可能被检查应用包的人以明文形式访问。诸如 react-native-dotenvreact-native-config 等工具非常适合添加环境特定变量(如 API 端点),但切勿将其与常包含密钥和 API 密钥的服务端环境变量混淆。

若必须通过 API 密钥或密钥访问资源,最安全的做法是在应用与资源间构建协调层。可通过无服务器函数(例如 AWS Lambda 或 Google Cloud Functions)转发携带所需 API 密钥的请求。服务端代码中的密钥无法像应用代码中的密钥那样被 API 消费者获取。

对于持久化用户数据,应根据其敏感度选择合适的存储类型。 随着应用使用,你常需在设备上保存数据——无论是为支持离线使用、减少网络请求,还是保存用户会话间的访问令牌避免重复验证。

持久化 vs 非持久化 —— 持久化数据会写入设备磁盘,使得应用在多次启动间无需重新网络请求或用户输入即可读取。但这也增加了攻击者访问数据的风险。非持久化数据永不写入磁盘——自然无从访问!

Async Storage

Async Storage 是 React Native 社区维护的异步、未加密键值存储模块。应用间不共享存储:每个应用都有独立的沙盒环境,无法访问其他应用的数据。

Do use async storage when...Don't use async storage for...
Persisting non-sensitive data across app runsToken storage
Persisting Redux stateSecrets
Persisting GraphQL state
Storing global app-wide variables

开发者须知

Async Storage is the React Native equivalent of Local Storage from the web

安全存储

React Native 未内置敏感数据存储方案,但 Android 和 iOS 平台已有现成解决方案。

iOS - Keychain Services

Keychain Services 可安全存储用户的小块敏感信息,是保存证书、令牌、密码等不应存入 Async Storage 的敏感数据的理想场所。

Android - 安全的 Shared Preferences

Shared Preferences 是 Android 的持久化键值存储方案。其数据默认不加密,但 Encrypted Shared Preferences 封装了 Shared Preferences 类,可自动加密键值。

Android - Keystore

Android Keystore 系统通过容器存储加密密钥,大幅增加从设备提取密钥的难度。

要使用 iOS Keychain 服务或 Android 安全共享首选项,您可以自行编写桥接代码,也可以使用封装库提供统一 API(但需自行承担风险)。以下是一些值得考虑的库:

警惕无意间存储或暴露敏感信息。这种情况可能意外发生,例如将敏感表单数据保存在 redux 状态中,并将整个状态树持久化到 Async Storage。或是将用户令牌和个人信息发送给 Sentry 或 Crashlytics 等应用监控服务。

身份验证与深度链接

移动应用存在一个网页环境中不存在的独特漏洞:深度链接。深度链接是从外部来源直接向原生应用发送数据的方式。深度链接格式如 app://,其中 app 是您的应用协议,// 后的内容可用于内部处理请求。

例如,构建电商应用时,您可以使用 app://products/1 深度链接到应用,并打开 ID 为 1 的商品详情页。这类似于网页 URL,但存在关键区别:

深度链接并不安全,切勿在其中发送任何敏感信息。

深度链接不安全的根源在于:URL 协议没有集中注册机制。应用开发者可通过在 iOS 的 Xcode 中配置 或 Android 的 添加 intent 使用几乎任意协议。

恶意应用可通过注册相同协议劫持您的深度链接,从而获取链接包含的数据。发送 app://products/1 这样的链接无害,但发送令牌会引发安全问题。

当操作系统需选择多个应用打开链接时,Android 会显示消歧对话框让用户选择。但在 iOS 上,操作系统会自行选择,用户对此毫不知情。苹果在后续 iOS 版本(iOS 11)中采取"先到先得"原则解决此问题,但该漏洞仍可通过其他方式利用(详见此处)。使用通用链接可在 iOS 中安全链接到应用内内容。

OAuth2 与重定向

OAuth2 认证协议如今极为流行,被誉为最完善的安全协议。OpenID Connect 协议也基于此构建。在 OAuth2 流程中,用户需通过第三方进行认证。成功完成后,第三方携带验证码重定向回请求应用,该验证码可交换为 JWT(即 JSON Web Token)。JWT 是用于在 Web 各方间安全传输信息的开放标准。

在网页环境中,重定向步骤是安全的,因为 Web URL 具有唯一性保证。但应用环境不同——如前所述,URL 协议没有集中注册机制!为解决此安全隐患,必须通过 PKCE 机制添加额外验证。

PKCE(发音为“Pixy”)代表密钥验证码交换(Proof of Key Code Exchange),是 OAuth 2 规范的扩展功能。它通过添加额外的安全层来验证认证请求和令牌交换请求是否来自同一个客户端。PKCE 使用 SHA 256 加密哈希算法,该算法能为任意大小的文本或文件生成唯一"签名",其特性包括:

  • 无论输入文件大小如何,输出长度始终保持不变

  • 相同输入必定产生相同结果

  • 单向不可逆(无法通过输出反推原始输入)

现在你将拥有两个关键值:

  • code_verifier - 由客户端生成的大型随机字符串

  • code_challenge - code_verifier 的 SHA 256 哈希值

在初始的 /authorize 请求中,客户端会同时发送其内存中保存的 code_verifier 对应的 code_challenge。当授权请求成功返回后,客户端还需发送用于生成该 code_challenge 的原始 code_verifier。身份提供者(IDP)将重新计算 code_challenge,核验其是否与首次 /authorize 请求中的设定值匹配,仅在验证通过时才发放访问令牌。

这种机制确保只有发起初始授权流程的应用才能成功用验证码交换 JWT 令牌。即使恶意应用获取了验证码,该验证码单独也无法发挥作用。要查看实际运作案例,请参考此示例

推荐使用的原生 OAuth 库是 react-native-app-auth。这个 SDK 用于与 OAuth2 服务商通信,封装了原生 AppAuth-iOSAppAuth-Android 库,并支持 PKCE 机制。

请注意:react-native-app-auth 仅在您的身份提供者支持 PKCE 时才能启用该功能。

采用 PKCE 的 OAuth2 流程

网络安全

您的 API 必须始终启用 SSL 加密。SSL 加密能防止数据在离开服务器到达客户端的过程中被明文读取。您可通过识别 https://(而非 http://)开头的端点来确认其安全性。

SSL 证书锁定

仅使用 https 端点仍可能导致数据被拦截。在 https 机制下,客户端仅信任能提供有效证书的服务端——该证书必须由客户端预装的受信任证书颁发机构(CA)签发。攻击者可利用此机制,在用户设备上安装恶意根 CA 证书,使客户端信任攻击者签发的所有证书。因此,仅依赖证书验证仍可能使您遭受中间人攻击

SSL 证书锁定是客户端可采用的防御技术,通过在开发阶段将受信任证书列表嵌入(或称"锁定")到客户端,确保仅接受使用可信证书签名的请求,拒绝任何自签名证书。

使用 SSL 证书锁定需注意证书有效期问题。证书通常每 1-2 年过期,到期后需同时在服务端和应用中更新。当服务端证书更新后,任何嵌入了旧证书的应用将立即停止工作。

总结

虽然不存在万无一失的安全防护方案,但通过有意识的努力和勤勉工作,您完全有可能显著降低应用程序遭受安全漏洞的风险。请根据应用存储数据的敏感性、用户规模以及黑客入侵账户后可能造成的损害程度,合理配置安全投入。最后谨记:那些从未被请求过的信息,其访问难度要大得多。