Android 网络安全配置 Codelab

应用通过互联网交换数据早已成为司空见惯的事情。由于应用可能与其不信任的服务器通信,因此在发送和接收可能属于敏感和隐私性质的信息时须格外小心。

构建内容

在此 Codelab 中,您将构建一个显示消息的应用。每条消息都应包含发送者的姓名、文本消息和指向其“个人资料照片”的网址。该应用将通过执行以下操作来显示这些消息:

  • 加载包含网络文本消息列表的 JSON 文件。
  • 加载每张个人资料照片,并将其显示在相应消息旁。

学习内容

  • 网络通信安全为什么很重要。
  • 如何使用 Volley 库发出网络请求。
  • 如何使用网络安全配置来帮助提高网络通信的安全性。
  • 如何修改一些高级网络安全配置选项,帮助进行开发和测试。
  • 了解最常见的网络安全问题之一,以及网络安全配置可以如何帮助防止这一问题发生。

所需条件

  • 最新版本的 Android Studio
  • 运行 Android 7.0(API 级别 24)或更高版本的 Android 设备或模拟器
  • Node.js(或可配置的网络服务器的访问权限)

如果您在学习本 Codelab 时遇到任何问题(代码错误、语法错误、内容含义不清等),请通过 Codelab 左下角的“报告错误”链接报告该问题。

下载代码

点击下面的链接可下载本 Codelab 的所有代码:

下载源代码

解压下载的 ZIP 文件。这将解压缩根文件夹 (android-network-secure-config),其中包含 Android Studio 项目 (SecureConfig/) 以及我们将在后续阶段使用 (server/) 的一些数据文件。

您还可以直接从 GitHub 查看代码:(从 master 分支开始。)

GitHub 代码库

我们还在每个步骤之后都准备了包含最终代码的分支。如果您遇到困难,请查看 GitHub 上的相应分支,或克隆整个代码库:https://github.com/googlecodelabs/android-network-security-config/branches/all

点击“加载”图标后,此应用会通过远程服务器从 JSON 文件加载消息、姓名和指向其“个人资料照片”的网址的列表。接下来,消息以列表形式显示,应用会从所引用的网址加载图像。

注意:我们在此 Codelab 中使用的应用仅用于演示目的。其所需错误处理量会少于生产环境中的错误处理量。

d9e465c94b420ea1.png

应用架构

该应用遵循 MVP 模式,以便将数据存储和网络访问 (model) 与逻辑 (presenter) 和显示 (view) 分开。

MainContract 类包含的合同对 View 与 Presenter 之间的接口进行了说明:

MainContract.java

/*
 * Copyright 2017 Google Inc. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.example.networksecurity;

import com.example.networksecurity.model.Post;

/**
 * Contract defining the interface between the View and Presenter.
 */
public interface MainContract {

    interface View {
        /**
         * Sets the presenter for interaction from the View.
         *
         * @param presenter
         */
        void setPresenter(Presenter presenter);

        /**
         * Displays or hides a loading indicator.
         *
         * @param isLoading If true, display a loading indicator, hide it otherwise.
         */
        void setLoadingPosts(boolean isLoading);

        /**
         * Displays a list of posts on screen.
         *
         * @param posts The posts to display. If null or empty, the list should not be shown.
         */
        void setPosts(Post[] posts);

        /**
         * Displays an error message on screen and optionally prints out the error to logcat.
         */
        void showError(String title, String error);

        /**
         * Hides the error message.
         *
         * @see #showError(String, String)
         */
        void hideError();

        /**
         * Displays an empty message and icon.
         *
         * @param showMessage If true, the message is show. If false, the message is hidden
         */
        void showNoPostsMessage(boolean showMessage);
    }

    interface Presenter {
        /**
         * Call to start the application. Sets up initial state.
         */
        void start();

        /**
         * Loads post for display.
         */
        void loadPosts();

        /**
         * An error was encountered during the loading of profile images.
         */
        void onLoadPostImageError(String error, Exception e);
    }

}

应用配置

出于演示目的,我们已停用该应用的所有网络缓存。理想情况下,在生产环境中,该应用会利用本地缓存限制远程网络请求的数量。

gradle.properties 文件包含用以加载消息列表的网址:

gradle.properties

postsUrl="http://storage.googleapis.com/network-security-conf-codelab.appspot.com/v1/posts.json"

构建和运行应用

  1. 启动 Android Studio 并将 SecureConfig 目录作为 Android 项目打开。
  2. 点击“运行”以启动应用:e15973f44eed7cc2.png

以下应用屏幕截图为其在设备上显示的样子:

63300e7e262bd161.png

在这一步中,我们将设置基本的网络安全配置,并观察违反配置中的任何规则时发生的错误。

概览

借助网络安全配置,应用可以通过声明性配置文件自定义其网络安全设置。整个配置都包含在此 XML 文件中,并且无需更改代码。

它支持对以下内容进行配置:

  • 选择停用明文流量:停用明文流量。
  • 自定义信任锚:指定应用信任的证书授权机构和来源。
  • 仅调试替换:在不影响发布 build 的情况下,安全地调试安全连接。
  • 证书锁定:限制仅与特定证书建立安全连接。

该文件可以按网域划分,从而将网络安全设置应用于所有网址或仅应用到特定网域。

网络安全配置适用于 Android 7.0(API 级别 24)及更高版本。

创建网络安全配置 XML 文件

创建名为 network_security_config.xml 的新 xml 资源文件

在左侧的 Android Project 面板中,右键点击 res,然后选择 New > Android 资源文件

35db6786b96a6980.png

设置以下选项,然后点击 OK

文件名

network_security_config.xml

资源类型

XML

根元素

network-security-config

目录名称

xml

36ae9e950fe66f1c.png

打开 xml/network_security_config.xml 文件(如果文件未自动打开)。

将其内容替换为以下代码段:

res/xml/network_security_config.xml

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <base-config cleartextTrafficPermitted="false" >
    </base-config>
</network-security-config>

此配置适用于应用的基本配置或默认安全配置,并停用所有明文流量。

启用网络安全配置

接下来,在 AndroidManifest.xml 文件中添加对应用配置的引用。

打开 AndroidManifest.xml 文件并在其中找到 application 元素。

首先,移除设置 android:usesCleartextTraffic="true" 属性的行。

接下来,将 android:networkSecurityConfig 属性添加到 AndroidManifest 中的 application 元素,并引用 network_security_config XML 文件资源:@xml/network_security_config

删除并添加以上两个属性后,起始应用标记应如下所示:

AndroidManifest.xml

<application
    android:networkSecurityConfig="@xml/network_security_config"
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:roundIcon="@mipmap/ic_launcher_round"
    android:supportsRtl="true"
    android:theme="@style/AppTheme"
    android:fullBackupContent="false"
    tools:ignore="GoogleAppIndexingWarning">
    ...

编译并运行应用

编译并运行应用。

您将看到一条错误提示,表示该应用尝试通过明文连接加载数据!

98d8a173d5293742.png

在 logcat 中,您会注意到以下错误:

java.io.IOException: Cleartext HTTP traffic to storage.googleapis.com not permitted

应用未加载数据,因为它仍然配置为从未加密的 HTTP 连接加载消息列表。在 gradle.properties 文件中配置的网址指向一个不使用 TLS 的 HTTP 服务器!

我们将此网址更改为其他服务器,并通过安全的 HTTPS 连接加载数据。

按如下方式更改 gradle.properties 文件:

gradle.properties

postsUrl="https://storage.googleapis.com/network-security-conf-codelab.appspot.com/v1/posts.json"

(注意网址中的 https 协议。)

您可能需要重新构建项目才能使此更改生效。从菜单中选择 Build > Rebuild

再次运行应用。现在,您将看到数据加载,因为网络请求使用了 HTTPS 连接:

63300e7e262bd161.png

当应用通过不安全的连接发出请求时,网络安全配置可以防范漏洞。

网络安全配置解决的另一个常见问题是,服务器端变更会影响加载到 Android 应用中的网址。例如,在我们的应用中,假设服务器开始为个人资料图片返回不安全的 HTTP 网址,而不是安全的 HTTPS 网址。然后,强制执行 HTTPS 连接的网络安全配置将引发异常,原因是在运行时无法满足此要求。

更新应用后端

您可能还记得,应用首先会加载一个消息列表,每条消息都会引用个人资料照片的网址。

假设应用消耗的数据发生了变化,导致应用请求不同的图片网址。我们可以通过修改后端数据网址来模拟此更改。

按如下方式更改 gradle.properties 文件:

gradle.properties

postsUrl="https://storage.googleapis.com/network-security-conf-codelab.appspot.com/v2/posts.json"

(请注意路径中的“v2”!)

您可能需要重新构建项目才能使此更改生效。从菜单中选择 Build > Rebuild

您可以从浏览器中访问“新”后端,以查看修改后的 JSON 文件。请注意,所有引用的网址都使用 HTTP 而非 HTTPS。

运行应用并检查错误

编译并运行应用。

应用可以加载消息,但无法加载图片。检查应用和 logcat 中的错误消息,了解原因:

a2a98a842e99168d.png

java.io.IOException: Cleartext HTTP traffic to storage.googleapis.com not permitted

应用仍使用 HTTPS 访问 JSON 文件。然而,JSON 文件中指向个人资料图片的链接使用的是 HTTP 地址,因此应用会尝试通过(不安全的)HTTP 加载图片。

保护数据

网络安全配置已成功阻止了意外的数据泄露。应用阻止了连接尝试,而不是尝试访问不安全的数据。

设想这样一个场景,即后端的更改在发布之前未经过充分测试。将网络安全配置应用到您的 Android 应用中,可以在出现类似问题时及时捕捉问题,即使在应用发布之后也是如此。

更改后端以修复应用

将后端网址更改为已“修复”的新版本。本示例使用正确的 HTTPS 网址引用配置文件图像来模拟修复。

更改 gradle.properties 文件中的后端网址并刷新项目:

gradle.properties

postsUrl="https://storage.googleapis.com/network-security-conf-codelab.appspot.com/v3/posts.json"

(请注意路径中的 v3!)

再次运行应用。现在,它将按预期运行:

63300e7e262bd161.png

到目前为止,我们已在 base-config 中指定网络安全配置,这会将配置应用于应用尝试建立的所有连接。

您可以通过指定 domain-config 元素,来替换特定目标的配置。domain-config 为一组特定网域声明了配置选项。

我们将应用中的网络安全配置更新为以下内容:

res/xml/network_security_config.xml

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <base-config cleartextTrafficPermitted="false" />
    <domain-config cleartextTrafficPermitted="true">
        <domain includeSubdomains="true">localhost</domain>
    </domain-config>
</network-security-config>

此配置将 base-config 应用于所有网域,“localhost”网域及其子网域除外,我们对其应用了不同的配置。

在这里,基本配置会阻止所有网域的明文流量。但网域配置会替换该规则,因此应用能够使用明文访问 localhost。

使用本地 HTTP 服务器进行测试

既然应用现在可以使用明文访问 localhost,我们将启动本地网络服务器并测试此访问协议。

Node.JS、Python 和 Perl 等多种工具都可用于托管非常基本的网络服务器。在此 Codelab 中,我们将使用 http-server Node.JS 模块为应用提供数据。

  1. 打开终端并安装 http-server
npm install http-server -g
  1. 导航到您已签出代码的目录,然后转到 server/ 目录:
cd server/
  1. 启动网络服务器,并提供位于 data/ 目录下的文件:
http-server ./data -p 8080
  1. 打开网络浏览器并导航到 http://localhost:8080,以验证您能否访问这些文件并查看“posts.json”文件:

934e48553bcc48e7.png

  1. 接下来,将端口 8080 从设备转接到本地计算机。在其他终端窗口中运行以下命令:
adb reverse tcp:8080 tcp:8080

您的应用现在可以从 Android 设备访问“localhost:8080”。

  1. 更改用于在应用中加载数据的网址,以指向 localhost 上的新服务器。按如下所示更改 gradle.properties 文件:(请注意,更改此文件后,您可能需要执行 gradle 项目同步。)

gradle.properties

postsUrl="http://localhost:8080/posts.json"
  1. 运行应用并验证数据是否从本地计算机加载。您可以尝试修改 data/posts.json 文件并刷新应用,以确认新配置是否按预期运行。

63300e7e262bd161.png

补充内容 - 网域配置

适用于特定网域的配置选项在 domain-config 元素中定义。此元素可以包含多个 domain 条目,以指定 domain-config 规则应该应用到的位置。如果多个 domain-config 元素包含类似的 domain 条目,则网络安全配置会根据匹配字符数量选择要应用于给定网址的配置。使用的配置包含与网址匹配最多(连续)字符的 domain 条目。

一个网域配置可以应用于多个网域,也可能应用于其子网域。

以下示例显示了包含多个网域的网络安全配置。(我们并未变更应用,这只是一个示例!)

<network-security-config>
    <domain-config>
        <domain includeSubdomains="true">secure.example.com</domain>
        <domain includeSubdomains="true">cdn.example.com</domain>
        <trust-anchors>
            <certificates src="@raw/trusted_roots"/>
        </trust-anchors>
    </domain-config>
</network-security-config>

有关详情,请参阅配置文件格式定义

在您开发和测试旨在通过 HTTPS 发出请求的应用时,您可能需要像在上一步所做的那样,将其连接到本地网络服务器或测试环境。

您不需要为此用例允许使用明文流量或是修改代码,网络安全配置中的 debug-override 选项允许您设置仅当应用在调试模式下运行时才适用的安全选项,也就是当 android:debuggable 为 true 时。相比使用条件代码,安全性显著提升,因为其具有明确的仅调试定义。Play 商店也会阻止上传可调试的应用,这使得此选项更加安全。

在本地网络服务器上启用 SSL

之前,我们启动了本地网络服务器,它在端口 8080 通过 HTTP 提供数据。我们现在将生成一个自签名 SSL 证书,并使用该证书通过 HTTPS 传送数据:

  1. 通过在终端窗口中切换到 server/ 目录,然后执行以下命令来生成证书:(如果您仍在运行 http-server,可以按 [CTRL] + [C] 立即停止。)
# Run these commands from inside the server/ directory!

# Create a certificate authority
openssl genrsa -out root-ca.privkey.pem 2048
# Sign the certificate authority
openssl req -x509 -new -nodes -days 100 -key root-ca.privkey.pem -out root-ca.cert.pem -subj "/C=US/O=Debug certificate/CN=localhost" -extensions v3_ca -config openssl_config.txt
# create DER format crt for Android
openssl x509 -outform der -in root-ca.cert.pem -out debug_certificate.crt

此操作会生成一个证书授权机构,签署它,并生成 Android 所需的 DER 格式的证书。

  1. 使用新生成的证书通过 HTTPS 启动网络服务器:
http-server ./data --ssl --cert root-ca.cert.pem --key root-ca.privkey.pem

更新后端网址

更改应用以通过 HTTPS 访问本地主机服务器。

更改 gradle.properties 文件:

gradle.properties

postsUrl="https://localhost:8080/posts.json"

编译并运行应用。

由于服务器证书无效,应用将运行失败

3bcce1390e354724.png

java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.

应用无法访问网络服务器,因为服务器使用的自签名证书不被信任为系统的一部分。我们会在下一步中为本地主机网域添加自签名证书,而不是停用 HTTPS。

引用自定义证书授权机构

现在,网络服务器使用默认不被任何设备接受的自签名证书授权机构 (CA) 来提供数据。如果您通过浏览器访问服务器,会发现安全警告:https://localhost:8080

898b69ea4fe9bc21.png

接下来,我们将使用网络安全配置中的 debug-overrides 选项,仅允许 localhost 网域使用此自定义证书授权机构:

  1. 更改 xml/network_security_config.xml 文件,使其包含以下内容:

res/xml/network_security_config.xml

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <base-config cleartextTrafficPermitted="false" />
    <debug-overrides>
        <trust-anchors>
            <!-- Trust a debug certificate in addition to the system certificates -->
            <certificates src="system" />
            <certificates src="@raw/debug_certificate" />
        </trust-anchors>
    </debug-overrides>
</network-security-config>

此配置会停用明文网络流量,而对于调试 build,会启用系统提供的证书授权机构,以及存储在 res/raw 目录中的证书文件。

注意:调试配置会隐式添加 <certificates src="system" />,因此即使不执行此操作,应用也可以运行。我们添加它是为了向您展示在更高级配置中的添加方式。

  1. 接下来,从 server/ 目录下将文件“debug_certificate.crt”复制到 Android Studio 中应用的 res/raw 资源目录中。您还可以将文件拖放到 Android Studio 中的正确位置。

如果此目录不存在,您可能需要先创建此目录。

在服务器/ 目录中,您可以运行以下命令来执行此操作,否则,请使用文件管理器或 Android Studio 创建文件夹并将文件复制到正确的位置:

mkdir  ../SecureConfig/app/src/main/res/raw/
cp debug_certificate.crt ../SecureConfig/app/src/main/res/raw/

Android Studio 现在会列出 app/res/raw 下的 debug_certificate.crt 文件:

c3111ae17558e167.png

运行应用

编译并运行应用。应用现在使用自签名调试证书通过 HTTPS 访问我们的本地网络服务器。

如果您遇到错误,请仔细查看 logcat 输出并确保已使用新的命令行选项重新启动 http-server。同时,请确认 debug_certificate.crt 文件位于正确的位置 (res/raw/debug_certificate.crt)。

63300e7e262bd161.png

网络安全配置支持更多高级功能,包括:

使用这些功能时,请查看文档以了解最佳做法和限制的详细信息。

提升应用的安全性!

在本 Codelab 中,您已了解了如何使用网络安全配置来提高 Android 应用的安全性。考虑一下您自己的应用如何使用这些功能,以及在测试和开发中如何受益于功能更强大的调试配置。

了解详情