跳到主要内容

安全

在构建应用程序时,安全性常常被忽视。确实,构建完全不可穿透的软件是不可能的——我们尚未发明出完全不可穿透的锁(毕竟银行金库仍然会被闯入)。然而,遭受恶意攻击或暴露安全漏洞的可能性与您愿意为保护应用程序免受此类事件所付出的努力成反比。虽然普通的挂锁可以被撬开,但它仍然比橱柜挂钩更难逾越!

在本指南中,您将了解存储敏感信息、身份验证、网络安全以及有助于保护您的应用程序的工具的最佳实践。这不是一份飞行前检查清单——它是一份选项目录,其中每个选项都将有助于进一步保护您的应用程序和用户。

存储敏感信息

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

如果您必须拥有 API 密钥或秘密才能从您的应用访问某些资源,最安全的方法是在您的应用和资源之间构建一个编排层。这可能是一个无服务器函数(例如使用 AWS Lambda 或 Google Cloud Functions),它可以转发带有所需 API 密钥或秘密的请求。服务器端代码中的秘密不能像您应用代码中的秘密那样被 API 消费者访问。

对于持久化的用户数据,请根据其敏感性选择正确的存储类型。随着您的应用被使用,您经常会发现需要在设备上保存数据,无论是为了支持您的应用离线使用、减少网络请求,还是为了在会话之间保存用户的访问令牌,这样他们每次使用应用时就不必重新进行身份验证。

持久化与非持久化——持久化数据写入到设备的磁盘,这使得您的应用程序可以在应用程序启动时读取数据,而无需再次进行网络请求来获取或要求用户重新输入。但这也会使数据更容易受到攻击者的访问。非持久化数据从不写入磁盘——因此没有数据可以访问!

异步存储

Async Storage 是一个由社区维护的 React Native 模块,提供了一个异步、未加密的键值存储。Async Storage 不在应用程序之间共享:每个应用程序都有自己的沙盒环境,并且无权访问来自其他应用程序的数据。

在以下情况下使用异步存储:请勿将异步存储用于:
跨应用程序运行持久化非敏感数据令牌存储
持久化 Redux 状态机密
持久化 GraphQL 状态
存储全局应用程序范围变量

开发者说明

Async Storage 相当于网页中的 Local Storage。

安全存储

React Native 本身不提供任何存储敏感数据的方法。然而,Android 和 iOS 平台已经有现成的解决方案。

iOS - 钥匙串服务

钥匙串服务允许您安全地存储少量敏感的用户信息。这是存储证书、令牌、密码以及任何不属于 Async Storage 的其他敏感信息的理想位置。

安卓 - 安全共享偏好设置

Shared Preferences 是 Android 平台上持久性键值数据存储的等效物。Shared Preferences 中的数据默认不加密,但 Encrypted Shared Preferences 封装了 Android 的 Shared Preferences 类,并自动加密键和值。

安卓 - 密钥库

Android Keystore 系统允许您将加密密钥存储在容器中,从而更难从设备中提取。

为了使用 iOS 钥匙串服务或 Android 安全共享偏好设置,您可以自行编写桥接,或使用一个为您封装它们并提供统一 API 的库,但风险自负。一些可考虑的库有:

注意避免无意中存储或暴露敏感信息。 这可能会偶然发生,例如将敏感表单数据保存在 Redux 状态中并将整个状态树持久化到 Async Storage。或者将用户令牌和个人信息发送到 Sentry 或 Crashlytics 等应用程序监控服务。

身份验证与深度链接

移动应用程序存在一个网页上不存在的独特漏洞:深度链接。深度链接是一种将数据从外部源直接发送到原生应用程序的方式。一个深度链接看起来像 `app://`,其中 `app` 是您的应用程序方案,`//` 后的任何内容都可以在内部用于处理请求。

例如,如果您正在构建一个电子商务应用程序,您可以使用 `app://products/1` 来深度链接到您的应用程序并打开产品 ID 为 1 的产品详情页。您可以将这些链接视为网络上的 URL,但有一个关键区别:

深度链接不安全,您绝不应在其中发送任何敏感信息。

深度链接不安全的原因在于,没有集中化的方法来注册 URL 方案。作为应用程序开发人员,您可以通过在 Xcode 中配置 iOS 或在 Android 上添加 intent 来使用您选择的几乎任何 URL 方案。

没有什么能阻止恶意应用程序通过注册相同的方案来劫持您的深度链接,然后获取链接中包含的数据。发送像 `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”,代表“密钥交换证明”,是 OAuth 2 规范的扩展。它涉及添加额外的安全层,用于验证身份验证和令牌交换请求来自同一个客户端。PKCE 使用 SHA 256 加密哈希算法。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。React-native-app-auth 是一个用于与 OAuth2 提供商通信的 SDK。它封装了原生的 AppAuth-iOSAppAuth-Android 库,并且可以支持 PKCE。

只有当您的身份提供商支持 PKCE 时,React-native-app-auth 才能支持它。

OAuth2 with PKCE

网络安全

您的 API 应该始终使用 SSL 加密。SSL 加密可防止请求数据在离开服务器到到达客户端之间以明文形式被读取。您会知道端点是安全的,因为它以 https:// 而不是 http:// 开头。

SSL 绑定

使用 HTTPS 端点仍可能使您的数据容易受到拦截。使用 HTTPS 时,客户端只会信任服务器,如果服务器能提供由客户端预安装的受信任证书颁发机构签名的有效证书。攻击者可以利用这一点,通过在用户设备上安装恶意根 CA 证书,这样客户端就会信任所有由攻击者签名的证书。因此,仅依赖证书仍可能使您容易受到中间人攻击

SSL 绑定(SSL pinning)是一种可以在客户端使用的技术,以避免这种攻击。它的工作原理是在开发过程中将受信任证书列表嵌入(或绑定)到客户端,这样只有使用其中一个受信任证书签名的请求才会被接受,任何自签名证书都不会被接受。

使用 SSL 绑定时,您应注意证书过期。证书每 1-2 年过期一次,当证书过期时,需要在应用程序和服务器上同时更新。一旦服务器上的证书更新,任何嵌入旧证书的应用程序将停止工作。

总结

没有万无一失的安全处理方法,但通过有意识的努力和勤勉,可以显著降低应用程序发生安全漏洞的可能性。根据应用程序中存储数据的敏感性、用户数量以及黑客获取账户权限后可能造成的损害,投入相应的安全保障。请记住:从未被请求的信息,获取起来要困难得多。