欢迎参加我们将于 6 月 3 日举行的 #Android11:Beta 版发布会

应用安全性最佳做法

提高应用的安全性,有助于维护用户的信任和设备完整性。

本页介绍了若干最佳做法,可以极大地改善您的应用安全。

执行安全通信

如果您对您的应用与其他应用或您的应用与网站之间交换的数据采取保护措施,则可以提升应用的稳定性,并保护您发送和接收的数据。

使用隐式 Intent 和不支持导出的内容提供程序

显示应用选择器

如果隐式 Intent 可以在用户设备上启动至少两个可能的应用,请明确显示应用选择器。此互动策略允许用户向信任的应用传输敏感信息。

Kotlin

    val intent = Intent(ACTION_SEND)
    val possibleActivitiesList: List<ResolveInfo> =
            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 =
            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 中,<provider> 元素的 android:exported 属性默认设为 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 码/密码/图案或生物识别凭据,如使用人脸识别或指纹识别。

要详细了解如何索要生物识别凭据,请参阅生物识别身份验证指南

应用网络安全措施

下面几个小节将介绍如何提升应用的网络安全性。

使用 SSL 流量

如果您的应用与某个网络服务器通信,而该服务器具有由公认的可信 CA 颁发的证书,则 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,您可以在配置文件中声明您的网络安全设置。通过这种方式,您可以在不修改任何应用代码的前提下创建配置。

要向您的应用添加网络安全配置文件,请按以下步骤操作:

  1. 在应用的清单中声明配置:
  2.     <manifest ... >
            <application
                android:networkSecurityConfig="@xml/network_security_config"
                ... >
                <!-- Place child elements of <application> element here. -->
            </application>
        </manifest>
        
  3. 添加 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>
        

相关信息网络安全配置

创建您自己的信任管理器

您的 SSL 检查器不应接受所有的证书。如果您的用例符合以下情形之一,您可能需要设置信任管理器来处理收到的所有 SSL 警告:

  • 您与之进行通信的网络服务器具有由新的或自定义的 CA 签名的证书。
  • 您所使用的设备不信任该 CA。
  • 您无法使用网络安全配置

要详细了解如何完成这些步骤,请参阅有关处理未知证书颁发机构的讨论。

相关信息:

谨慎使用 WebView 对象

尽可能仅在 WebView 对象中加载列入白名单的内容。换言之,您的应用中的 WebView 对象不应允许用户导航到超出您控制范围的网站。

此外,除非您可以完全控制并信任应用的 WebView 对象中的内容,否则绝不要启用 JavaScript 接口支持

使用 HTML 消息通道

如果您的应用必须在运行 Android 6.0(API 级别 23)及更高版本的设备上使用 JavaScript 接口支持,请使用 HTML 消息通道(而非 evaluateJavascript())在网站和您的应用之间进行通信,如以下代码段中所示:

Kotlin

    val myWebView: WebView = findViewById(R.id.webview)

    // messagePorts[0] and messagePorts[1] represent the two ports.
    // They are already tangled to 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);

    // messagePorts[0] and messagePorts[1] represent the two ports.
    // They are already tangled to 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_CONTACTSWRITE_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 的内容后,发出调用的应用就可以获得所选资源的相关权限。

相关信息:

在应用之间安全地共享数据

遵循以下最佳做法,以更安全的方式与其他应用共享您应用的内容:

以下代码段展示了如何使用 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);
    }
    

注意:以 Android 10(API 级别 29)及更高版本为目标平台的不可信应用,将无法对应用主目录中的文件调用 exec()。这种从可写应用的主目录执行文件的行为违反了 W^X。这些应用应仅加载嵌入到应用 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.
    }
    

相关信息:

谨慎使用外部存储

默认情况下,Android 系统不会强制对存储在外部存储中的数据实施安全限制,存储介质本身也不保证始终与设备保持连接。因此,您应该应用下列安全措施,以确保对外部存储内的信息的安全访问。

使用作用域目录访问

如果您的应用只需要访问设备外部存储中的某一个目录,您就可以使用作用域目录访问相应地限制应用对设备外部存储的访问。为方便用户,您的应用应该保存访问该目录的 URI,以便用户不必在应用每次尝试访问该目录时都要批准访问权限。

注意:如果您对外部存储中的某个目录使用作用域目录访问,请注意用户可能会在应用运行期间弹出包含此存储的介质。您应该添加相应的逻辑,以安全地处理此用户行为引起的 Environment.getExternalStorageState() 返回值变化。

以下代码段展示了对设备主要共享存储中的图片目录使用作用域目录访问的方法:

Kotlin

    private const val PICTURES_DIR_ACCESS_REQUEST_CODE = 42

    ...

    private fun accessExternalPicturesDirectory() {
        val intent: Intent = (getSystemService(Context.STORAGE_SERVICE) as StorageManager)
                .primaryStorageVolume.createAccessIntent(Environment.DIRECTORY_PICTURES)
        startActivityForResult(intent, PICTURES_DIR_ACCESS_REQUEST_CODE)
    }

    ...

    override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) {
        if (requestCode == PICTURES_DIR_ACCESS_REQUEST_CODE && resultCode == Activity.RESULT_OK) {

            // User approved access to scoped directory.
            if (resultData != null) {
                val picturesDirUri: Uri = resultData.data

                // Save user's approval for accessing this directory
                // in your app.
                contentResolver.takePersistableUriPermission(
                        picturesDirUri,
                        Intent.FLAG_GRANT_READ_URI_PERMISSION
                )
            }
        }
    }
    

Java

    private static final int PICTURES_DIR_ACCESS_REQUEST_CODE = 42;

    private void accessExternalPicturesDirectory() {
      StorageManager sm =
              (StorageManager) getSystemService(Context.STORAGE_SERVICE);
      StorageVolume volume = sm.getPrimaryStorageVolume();
      Intent intent =
              volume.createAccessIntent(Environment.DIRECTORY_PICTURES);
      startActivityForResult(intent, PICTURES_DIR_ACCESS_REQUEST_CODE);
    }

    ...

    @Override
    public void onActivityResult(int requestCode, int resultCode,
            Intent resultData) {
        if (requestCode == PICTURES_DIR_ACCESS_REQUEST_CODE &&
                resultCode == Activity.RESULT_OK) {

            // User approved access to scoped directory.
            if (resultData != null) {
                Uri picturesDirUri = resultData.getData();

                // Save user's approval for accessing this directory
                // in your app.
                ContentResolver myContentResolver = getContentResolver();
                myContentResolver.takePersistableUriPermission(picturesDirUri,
                        Intent.FLAG_GRANT_READ_URI_PERMISSION);
            }
        }
    }
    

警告:若无必要,不要将 null 传递给 createAccessIntent(),因为这会授予您的应用对 StorageManager 为应用找到的整个卷的访问权限。

相关信息:

检查数据有效性

如果您的应用使用外部存储中的数据,请确保数据的内容没有被损坏或修改。您的应用还应包含相应的逻辑,以处理不再具有稳定格式的文件。

以下示例展示了用于检查文件有效性的权限和逻辑:

AndroidManifest.xml

    <manifest ... >
        <!-- Apps on devices running Android 4.4 (API level 19) or higher cannot
             access external storage outside their own "sandboxed" directory, so
             the READ_EXTERNAL_STORAGE (and WRITE_EXTERNAL_STORAGE) permissions
             aren't necessary. -->
        <uses-permission
              android:name="android.permission.READ_EXTERNAL_STORAGE"
              android:maxSdkVersion="18" />
        ...
    </manifest>

    

MyFileValidityChecker

Kotlin

    private val UNAVAILABLE_STORAGE_STATES: Set<String> =
            setOf(MEDIA_REMOVED, MEDIA_UNMOUNTED, MEDIA_BAD_REMOVAL, MEDIA_UNMOUNTABLE)
    ...
    val ringtone = File(getExternalFilesDir(DIRECTORY_RINGTONES), "my_awesome_new_ringtone.m4a")
    when {
        isExternalStorageEmulated(ringtone) -> {
            Log.e(TAG, "External storage is not present")
        }
        UNAVAILABLE_STORAGE_STATES.contains(getExternalStorageState(ringtone)) -> {
            Log.e(TAG, "External storage is not available")
        }
        else -> {
            val fis = FileInputStream(ringtone)

            // available() determines the approximate number of bytes that
            // can be read without blocking.
            val bytesAvailable: Int = fis.available()
            val fileBuffer = ByteArray(bytesAvailable)
            StringBuilder(bytesAvailable).apply {
                while (fis.read(fileBuffer) != -1) {
                    append(fileBuffer)
                }
                // Implement appropriate logic for checking a file's validity.
                checkFileValidity(this)
            }
        }
    }
    

Java

    File ringtone = new File(getExternalFilesDir(DIRECTORY_RINGTONES,
            "my_awesome_new_ringtone.m4a"));
    if (isExternalStorageEmulated(ringtone)) {
        Logger.e(TAG, "External storage is not present");
    } else if (getExternalStorageState(ringtone) == MEDIA_REMOVED
            | MEDIA_UNMOUNTED | MEDIA_BAD_REMOVAL | MEDIA_UNMOUNTABLE) {
        Logger.e(TAG, "External storage is not available");
    } else {
        FileInputStream fis = new FileInputStream(ringtone);

        // available() determines the approximate number of bytes that
        // can be read without blocking.
        int bytesAvailable = fis.available();
        StringBuilder fileContents = new StringBuilder(bytesAvailable);
        byte[] fileBuffer = new byte[bytesAvailable];
        while (fis.read(fileBuffer) != -1) {
            fileContents.append(fileBuffer);
        }

        // Implement appropriate logic for checking a file's validity.
        checkFileValidity(fileContents);
    }
    

相关信息:

仅将非敏感数据存储在缓存文件中

为了加快对非敏感应用数据的访问,您可以将它存储在设备的缓存中。如果缓存大小超过 1MB,请使用 getExternalCacheDir();否则,请使用 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() 将应用缓存放在共享存储中,用户可能会在应用运行期间弹出包含此存储的介质。您应该添加相应的逻辑,以安全地处理此用户行为导致的缓存未命中。

注意:这些文件没有实施任何强制的安全措施。因此,任何拥有 WRITE_EXTERNAL_STORAGE 权限的应用都可访问此缓存中的内容。

相关信息:保存缓存文件

在私有模式下使用 SharedPreferences

在使用 getSharedPreferences() 创建或访问您的应用的 SharedPreferences 对象时,请使用 MODE_PRIVATE。这样,则只有您的应用才能访问共享偏好设置文件中的信息。

如果想要在应用之间共享数据,请不要使用 SharedPreferences 对象,而应该执行必要的步骤,在应用之间安全地共享数据

相关信息:使用共享偏好设置

确保服务和依赖项处于最新状态

大多数应用使用外部库和设备系统信息来完成特定的任务。通过及时更新应用的依赖项,您可以提高这些通信点的安全性。

检查 Google Play 服务安全提供程序

注意:本节仅适用于针对安装了 Google Play 服务的设备而设计的应用。

如果您的应用使用 Google Play 服务,请确保该服务在安装有您应用的设备上处于最新状态。这项检查应在界面线程外异步执行。如果设备不是最新状态,您的应用应触发授权错误。

要确定安装有您应用的设备上的 Google Play 服务是否为最新状态,请按照更新您的安全提供程序以防范 SSL 攻击的指南中的步骤操作。

相关信息:

更新所有应用依赖项

在部署您的应用前,请确保所有库、SDK 和其他依赖项都处于最新状态:

  • 对于 Android SDK 等第一方依赖项,请使用 Android Studio 中提供的更新工具,如 SDK 管理器
  • 对于第三方依赖项,请检查您的应用所用的库的网站,并安装所有可用的更新和安全补丁。

相关信息:添加构建依赖项

更多信息

要详细了解如何提高应用的安全性,请查看以下资源:

其他资源

有关提高应用安全性的更多信息,请参考以下资源。

Codelab

博客