通过提高应用安全性,您可以帮助维护用户信任和设备完整性。
此页面介绍了一些最佳实践,这些实践对应用安全性有重大积极影响。
强制执行安全通信
当您保护应用之间或应用与网站之间交换的数据时,您可以提高应用的稳定性并保护您发送和接收的数据。
保护应用之间的通信
为了更安全地进行应用之间的通信,请使用带有应用选择器的隐式意图、基于签名的权限和非导出内容提供程序。
显示应用选择器
如果隐式意图可以在用户设备上启动至少两个可能的应用,则明确显示应用选择器。此交互策略允许用户将敏感信息转移到他们信任的应用。
Kotlin
val intent = Intent(Intent.ACTION_SEND) val possibleActivitiesList: List<ResolveInfo> = packageManager.queryIntentActivities(intent, PackageManager.MATCH_ALL) // Verify that an activity in at least two apps on the user's device // can handle the intent. Otherwise, start the intent only if an app // on the user's device can handle the intent. if (possibleActivitiesList.size > 1) { // Create intent to show chooser. // Title is something similar to "Share this photo with." val chooser = resources.getString(R.string.chooser_title).let { title -> Intent.createChooser(intent, title) } startActivity(chooser) } else if (intent.resolveActivity(packageManager) != null) { startActivity(intent) }
Java
Intent intent = new Intent(Intent.ACTION_SEND); List<ResolveInfo> possibleActivitiesList = getPackageManager() .queryIntentActivities(intent, PackageManager.MATCH_ALL); // Verify that an activity in at least two apps on the user's device // can handle the intent. Otherwise, start the intent only if an app // on the user's device can handle the intent. if (possibleActivitiesList.size() > 1) { // Create intent to show chooser. // Title is something similar to "Share this photo with." String title = getResources().getString(R.string.chooser_title); Intent chooser = Intent.createChooser(intent, title); startActivity(chooser); } else if (intent.resolveActivity(getPackageManager()) != null) { startActivity(intent); }
相关信息
应用基于签名的权限
在共享您控制或拥有的两个应用之间的数据时,请使用基于签名的权限。这些权限不需要用户确认,而是检查访问数据的应用是否使用相同的签名密钥进行签名。因此,这些权限提供了更简化的、更安全的用户体验。
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.myapp"> <permission android:name="my_custom_permission_name" android:protectionLevel="signature" />
相关信息
禁止访问您应用的内容提供程序
除非您打算将数据从您的应用发送到您不拥有的其他应用,否则请明确禁止其他开发者的应用访问您应用的 ContentProvider
对象。此设置在您的应用可能安装在运行 Android 4.1.1(API 级别 16)或更低版本的设备上的情况下尤其重要,因为 android:exported
属性的 <provider>
元素在这些版本的 Android 上默认情况下为 true
。
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.myapp"> <application ... > <provider android:name="android.support.v4.content.FileProvider" android:authorities="com.example.myapp.fileprovider" ... android:exported="false"> <!-- Place child elements of <provider> here. --> </provider> ... </application> </manifest>
在显示敏感信息之前请求凭据
当从用户请求凭据以便他们能够访问您的应用中的敏感信息或高级内容时,请要求 PIN/密码/图案或生物识别凭据,例如面部识别或指纹识别。
要了解有关如何请求生物识别凭据的更多信息,请参阅 关于生物识别身份验证的指南。
应用网络安全措施
以下部分介绍了如何提高应用的网络安全性。
使用 TLS 流量
如果您的应用与拥有由知名可信证书颁发机构 (CA) 颁发的证书的 Web 服务器进行通信,请使用以下 HTTPS 请求
Kotlin
val url = URL("https://www.google.com") val urlConnection = url.openConnection() as HttpsURLConnection urlConnection.connect() urlConnection.inputStream.use { ... }
Java
URL url = new URL("https://www.google.com"); HttpsURLConnection urlConnection = (HttpsURLConnection) url.openConnection(); urlConnection.connect(); InputStream in = urlConnection.getInputStream();
添加网络安全配置
如果您的应用使用新的或自定义 CA,您可以在配置文件中声明网络的安全设置。此过程允许您创建配置而无需修改任何应用代码。
要将网络安全配置文件添加到您的应用,请执行以下步骤
- 在您的应用清单中声明配置
-
添加一个 XML 资源文件,位于
res/xml/network_security_config.xml
。通过禁用明文指定对特定域的所有流量都必须使用 HTTPS
<network-security-config> <domain-config cleartextTrafficPermitted="false"> <domain includeSubdomains="true">secure.example.com</domain> ... </domain-config> </network-security-config>
在开发过程中,您可以使用
<debug-overrides>
元素显式允许用户安装的证书。此元素在调试和测试期间会覆盖应用程序的安全关键选项,而不会影响应用程序的发布配置。以下代码段展示了如何在应用程序的网络安全配置 XML 文件中定义此元素<network-security-config> <debug-overrides> <trust-anchors> <certificates src="user" /> </trust-anchors> </debug-overrides> </network-security-config>
<manifest ... > <application android:networkSecurityConfig="@xml/network_security_config" ... > <!-- Place child elements of <application> element here. --> </application> </manifest>
相关信息: 网络安全配置
创建您自己的信任管理器
您的 TLS 检查器不应该接受所有证书。您可能需要设置一个信任管理器并处理所有发生的 TLS 警告,如果您的用例符合以下任一条件
- 您正在与使用由新的或自定义 CA 签名的证书的 Web 服务器通信。
- 该 CA 未被您使用的设备信任。
- 您无法使用 网络安全配置.
要详细了解如何完成这些步骤,请参阅有关处理 未知证书颁发机构 的讨论。
相关信息
谨慎使用 WebView 对象
WebView
对象不应允许用户导航到不受您控制的网站。尽可能使用允许列表来限制应用程序 WebView
对象加载的内容。
此外,除非您完全控制并信任应用程序 WebView
对象中的内容,否则永远不要启用 JavaScript 接口支持。
使用 HTML 消息通道
如果您的应用程序必须在运行 Android 6.0(API 级别 23)及更高版本的设备上使用 JavaScript 接口支持,请使用 HTML 消息通道,而不是在网站和您的应用程序之间进行通信,如以下代码段所示
Kotlin
val myWebView: WebView = findViewById(R.id.webview) // channel[0] and channel[1] represent the two ports. // They are already entangled with each other and have been started. val channel: Array<out WebMessagePort> = myWebView.createWebMessageChannel() // Create handler for channel[0] to receive messages. channel[0].setWebMessageCallback(object : WebMessagePort.WebMessageCallback() { override fun onMessage(port: WebMessagePort, message: WebMessage) { Log.d(TAG, "On port $port, received this message: $message") } }) // Send a message from channel[1] to channel[0]. channel[1].postMessage(WebMessage("My secure message"))
Java
WebView myWebView = (WebView) findViewById(R.id.webview); // channel[0] and channel[1] represent the two ports. // They are already entangled with each other and have been started. WebMessagePort[] channel = myWebView.createWebMessageChannel(); // Create handler for channel[0] to receive messages. channel[0].setWebMessageCallback(new WebMessagePort.WebMessageCallback() { @Override public void onMessage(WebMessagePort port, WebMessage message) { Log.d(TAG, "On port " + port + ", received this message: " + message); } }); // Send a message from channel[1] to channel[0]. channel[1].postMessage(new WebMessage("My secure message"));
相关信息
提供正确的权限
仅请求应用程序正常运行所需的最低权限。尽可能在应用程序不再需要权限时放弃权限。
使用 Intent 推迟权限
尽可能避免在您的应用程序中添加权限来完成可以在其他应用程序中完成的操作。相反,使用 Intent 将请求推迟到已经拥有必要权限的其他应用程序。
以下示例展示了如何使用 Intent 将用户引导到联系人应用程序,而不是请求 READ_CONTACTS
和 WRITE_CONTACTS
权限
Kotlin
// Delegates the responsibility of creating the contact to a contacts app, // which has already been granted the appropriate WRITE_CONTACTS permission. Intent(Intent.ACTION_INSERT).apply { type = ContactsContract.Contacts.CONTENT_TYPE }.also { intent -> // Make sure that the user has a contacts app installed on their device. intent.resolveActivity(packageManager)?.run { startActivity(intent) } }
Java
// Delegates the responsibility of creating the contact to a contacts app, // which has already been granted the appropriate WRITE_CONTACTS permission. Intent insertContactIntent = new Intent(Intent.ACTION_INSERT); insertContactIntent.setType(ContactsContract.Contacts.CONTENT_TYPE); // Make sure that the user has a contacts app installed on their device. if (insertContactIntent.resolveActivity(getPackageManager()) != null) { startActivity(insertContactIntent); }
此外,如果您的应用程序需要执行基于文件的 I/O(例如访问存储或选择文件),则不需要特殊权限,因为系统可以代表您的应用程序完成操作。更重要的是,在用户选择特定 URI 的内容后,调用应用程序将获得对所选资源的权限。
相关信息
跨应用程序安全共享数据
遵循以下最佳实践,以更安全的方式与其他应用程序共享应用程序的内容
- 根据需要强制执行只读或只写权限。
- 使用
FLAG_GRANT_READ_URI_PERMISSION
和FLAG_GRANT_WRITE_URI_PERMISSION
标志,为客户端提供对数据的单次访问。 - 共享数据时,使用
content://
URI,而不是file://
URI。实例FileProvider
会为您执行此操作。
以下代码段展示了如何使用 URI 权限授予标志和内容提供者权限,在单独的 PDF 查看器应用程序中显示应用程序的 PDF 文件
Kotlin
// Create an Intent to launch a PDF viewer for a file owned by this app. Intent(Intent.ACTION_VIEW).apply { data = Uri.parse("content://com.example/personal-info.pdf") // This flag gives the started app read access to the file. addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) }.also { intent -> // Make sure that the user has a PDF viewer app installed on their device. intent.resolveActivity(packageManager)?.run { startActivity(intent) } }
Java
// Create an Intent to launch a PDF viewer for a file owned by this app. Intent viewPdfIntent = new Intent(Intent.ACTION_VIEW); viewPdfIntent.setData(Uri.parse("content://com.example/personal-info.pdf")); // This flag gives the started app read access to the file. viewPdfIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); // Make sure that the user has a PDF viewer app installed on their device. if (viewPdfIntent.resolveActivity(getPackageManager()) != null) { startActivity(viewPdfIntent); }
注意: 从可写应用程序主目录执行文件是 W^X 违规。因此,针对 Android 10(API 级别 29)及更高版本的不可信应用程序无法对应用程序主目录中的文件调用 exec()
,只能调用嵌入在应用程序 APK 文件中的二进制代码。此外,针对 Android 10 及更高版本的应用程序无法在内存中修改使用 dlopen()
打开的文件中的可执行代码。这包括任何具有文本重定位的共享对象 (.so
) 文件。
相关信息: android:grantUriPermissions
安全存储数据
尽管您的应用程序可能需要访问敏感的用户资料,但用户只有在信任您会妥善保护这些资料的情况下才会授予您的应用程序访问其资料的权限。
将私人数据存储在内部存储中
将所有私人用户数据存储在设备的内部存储中,该存储按应用程序进行沙盒化。您的应用程序不需要请求权限来查看这些文件,其他应用程序也无法访问这些文件。作为额外的安全措施,当用户卸载应用程序时,设备会删除应用程序保存在内部存储中的所有文件。
以下代码段演示了一种将数据写入内部存储的方法
Kotlin
// Creates a file with this name, or replaces an existing file // that has the same name. Note that the file name cannot contain // path separators. val FILE_NAME = "sensitive_info.txt" val fileContents = "This is some top-secret information!" File(filesDir, FILE_NAME).bufferedWriter().use { writer -> writer.write(fileContents) }
Java
// Creates a file with this name, or replaces an existing file // that has the same name. Note that the file name cannot contain // path separators. final String FILE_NAME = "sensitive_info.txt"; String fileContents = "This is some top-secret information!"; try (BufferedWriter writer = new BufferedWriter(new FileWriter(new File(getFilesDir(), FILE_NAME)))) { writer.write(fileContents); } catch (IOException e) { // Handle exception. }
以下代码段展示了逆向操作,从内部存储读取数据
Kotlin
val FILE_NAME = "sensitive_info.txt" val contents = File(filesDir, FILE_NAME).bufferedReader().useLines { lines -> lines.fold("") { working, line -> "$working\n$line" } }
Java
final String FILE_NAME = "sensitive_info.txt"; StringBuffer stringBuffer = new StringBuffer(); try (BufferedReader reader = new BufferedReader(new FileReader(new File(getFilesDir(), FILE_NAME)))) { String line = reader.readLine(); while (line != null) { stringBuffer.append(line).append('\n'); line = reader.readLine(); } } catch (IOException e) { // Handle exception. }
相关信息
根据用例将数据存储在外部存储中
将大型、非敏感文件(这些文件特定于您的应用程序,以及您的应用程序与其他应用程序共享的文件)存储在外部存储中。您使用的特定 API 取决于您的应用程序是设计用于访问特定于应用程序的文件还是访问共享文件。
如果文件不包含私人或敏感信息,但仅在您的应用程序中对用户有价值,请将文件存储在 外部存储上的特定于应用程序的目录 中。
如果您的应用程序需要访问或存储对其他应用程序有价值的文件,请根据您的用例使用以下 API 之一
- 媒体文件: 要存储和访问应用程序之间共享的图像、音频文件和视频,请 使用 Media Store API。
- 其他文件: 要存储和访问其他类型的共享文件(包括下载的文件),请 使用 Storage Access Framework。
检查存储卷的可用性
如果您的应用程序与可移动外部存储设备交互,请记住用户可能会在您的应用程序尝试访问它时移除存储设备。包含逻辑来 验证存储设备是否可用。
检查数据的有效性
如果您的应用程序使用来自外部存储的数据,请确保数据的內容未损坏或修改。包含逻辑来处理不再处于稳定格式的文件。
以下代码段包含一个哈希验证器的示例
Kotlin
val hash = calculateHash(stream) // Store "expectedHash" in a secure location. if (hash == expectedHash) { // Work with the content. } // Calculating the hash code can take quite a bit of time, so it shouldn't // be done on the main thread. suspend fun calculateHash(stream: InputStream): String { return withContext(Dispatchers.IO) { val digest = MessageDigest.getInstance("SHA-512") val digestStream = DigestInputStream(stream, digest) while (digestStream.read() != -1) { // The DigestInputStream does the work; nothing for us to do. } digest.digest().joinToString(":") { "%02x".format(it) } } }
Java
Executor threadPoolExecutor = Executors.newFixedThreadPool(4); private interface HashCallback { void onHashCalculated(@Nullable String hash); } boolean hashRunning = calculateHash(inputStream, threadPoolExecutor, hash -> { if (Objects.equals(hash, expectedHash)) { // Work with the content. } }); if (!hashRunning) { // There was an error setting up the hash function. } private boolean calculateHash(@NonNull InputStream stream, @NonNull Executor executor, @NonNull HashCallback hashCallback) { final MessageDigest digest; try { digest = MessageDigest.getInstance("SHA-512"); } catch (NoSuchAlgorithmException nsa) { return false; } // Calculating the hash code can take quite a bit of time, so it shouldn't // be done on the main thread. executor.execute(() -> { String hash; try (DigestInputStream digestStream = new DigestInputStream(stream, digest)) { while (digestStream.read() != -1) { // The DigestInputStream does the work; nothing for us to do. } StringBuilder builder = new StringBuilder(); for (byte aByte : digest.digest()) { builder.append(String.format("%02x", aByte)).append(':'); } hash = builder.substring(0, builder.length() - 1); } catch (IOException e) { hash = null; } final String calculatedHash = hash; runOnUiThread(() -> hashCallback.onHashCalculated(calculatedHash)); }); return true; }
仅将非敏感数据存储在缓存文件中
为了提供对非敏感应用程序数据的更快访问,请将其存储在设备的缓存中。对于大于 1 MB 的缓存,请使用 getExternalCacheDir()
。对于 1 MB 或更小的缓存,请使用 getCacheDir()
。这两种方法都会为您提供包含应用程序缓存数据的 File
对象。
以下代码段展示了如何缓存应用程序最近下载的文件
Kotlin
val cacheFile = File(myDownloadedFileUri).let { fileToCache -> File(cacheDir.path, fileToCache.name) }
Java
File cacheDir = getCacheDir(); File fileToCache = new File(myDownloadedFileUri); String fileToCacheName = fileToCache.getName(); File cacheFile = new File(cacheDir.getPath(), fileToCacheName);
注意: 如果您使用 getExternalCacheDir()
将应用程序的缓存放置在共享存储中,则用户可能会在应用程序运行时弹出包含此存储的媒体。包含逻辑来优雅地处理此用户行为导致的缓存丢失。
警告: 这些文件没有强制执行安全性。因此,任何针对 Android 10(API 级别 29)或更低版本的应用程序,如果具有 WRITE_EXTERNAL_STORAGE
权限,都可以访问此缓存的內容。
相关信息: 数据和文件存储概述
以私有模式使用 SharedPreferences
当使用 getSharedPreferences()
创建或访问应用程序的 SharedPreferences
对象时,请使用 MODE_PRIVATE
。这样,只有您的应用程序才能访问共享首选项文件中的信息。
如果您想跨应用程序共享数据,请不要使用 SharedPreferences
对象。相反,请按照 跨应用程序安全共享数据 的步骤操作。
Security 库还提供了类 EncryptedSharedPreferences,它封装了 SharedPreferences 类并自动加密密钥和值。
相关信息
保持服务和依赖项更新
大多数应用程序使用外部库和设备系统信息来完成专门的任务。通过保持应用程序的依赖项更新,您将使这些通信点更安全。
检查 Google Play 服务安全提供者
注意: 本节仅适用于针对已安装 Google Play 服务 的设备的应用程序。
如果您的应用程序使用 Google Play 服务,请确保它在安装应用程序的设备上已更新。异步执行检查,脱离 UI 线程。如果设备未更新,则会触发授权错误。
要确定 Google Play 服务是否在安装应用程序的设备上已更新,请按照有关 更新安全提供者以防止 SSL 漏洞 的指南中的步骤操作。
相关信息
更新所有应用程序依赖项
在部署应用程序之前,请确保所有库、SDK 和其他依赖项都是最新的
- 对于第一方依赖项(如 Android SDK),请使用 Android Studio 中的更新工具,例如 SDK 管理器。
- 对于第三方依赖项,请查看应用程序使用的库的网站,并安装任何可用的更新和安全补丁。
相关信息: 添加构建依赖项
更多信息
要详细了解如何使您的应用程序更安全,请查看以下资源
- 核心应用程序质量安全清单
- 应用程序安全改进计划
- YouTube 上的 Android 开发者频道
- Android 网络安全配置 Codelab
- Android 受保护的确认:将交易安全提升到一个新的水平