安卓应用安全指南-二-

99 阅读1小时+

安卓应用安全指南(二)

原文:Android Apps Security

协议:CC BY-NC-SA 4.0

六、与网络应用交互

在某些时候,您将不得不与 web 应用进行交互。无论您是与第三方的 RESTful API 对话,还是与您自己的后端 web 应用交换数据,您的移动应用都需要接受与其他应用交互的想法。当然,作为一个负责任的开发人员,您的工作是确保数据交换完成,这样攻击者就不能访问或更改属于最终用户的私有数据。在前面的章节中,当我们研究数据存储和加密时,我们花了时间来探索“静态数据”。在本章中,我们将讨论“传输中的数据”

最初,我不打算花太多时间讨论加密传输中的数据的好处。通常,SSL 或 TLS 将处理传输中数据的安全部分。然而,最近对荷兰 DigiNotar 认证机构的入侵让我重新考虑这个选项(更多信息见 en.wikipedia.org/wiki/DigiNo…)。最后,作为开发人员,我将让您来决定如何保护您的传输数据;但是很明显,最近的这次攻击让我想到,即使信任 SSL 也不总是最好的选择。因此,我将介绍一些与 web 应用安全性相关的主题,以及您的移动应用应该如何与这样的 web 应用进行交互。我还将简要介绍开放 Web 应用安全项目(OWASP );这是保护您的 web 应用的一个非常好的资源。

考虑清单 6-1 中的源代码有多安全。现在问问你自己,你会做些什么来使它更安全?(查看本章末尾的解决方案,并比较您自己的笔记,看看您的思路是否正确。)

清单 6-1 。 客户端登录

package net.zenconsult.android.examples;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;

import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;

import android.util.Log;

public class Login {
  private final String TAG = "HttpPost";

  public Login() {

  }

  public HttpResponse execute() {
       HttpClient client = new DefaultHttpClient();
       HttpPost post = new HttpPost(" [`logindemo1.appspot.com/logindemo`](http://logindemo1.appspot.com/logindemo)");
       HttpResponse response = null;

       // Post data with number of parameters
       List  <  NameValuePair  >       nvPairs = new ArrayList  <  NameValuePair  >  (2);
       nvPairs.add(new BasicNameValuePair("username", "sheran"));
  nvPairs.add(new BasicNameValuePair("password", "s3kretc0dez"));

       // Add post data to http post
       try {
       UrlEncodedFormEntity params = new UrlEncodedFormEntity(nvPairs);
       post.setEntity(params);
       response = client.execute(post);

       } catch (UnsupportedEncodingException e) {
       Log.e(TAG, "Unsupported Encoding used");
       } catch (ClientProtocolException e) {
       Log.e(TAG, "Client Protocol Exception");
       } catch (IOException e) {
       Log.e(TAG, "IOException in HttpPost");
       }
       return response;
       }

}

准备我们的环境

让我们从设置测试环境开始。我们显然需要一个现成的 web 应用托管基础设施。当我需要快速部署或测试 web 应用时,我通常依赖 Google App Engine。这为我节省了很多时间,而且我不必担心设置硬件、web 服务器和应用服务器。有了 Google App Engine,我可以用最小的设置开销开始编码。

让我们首先在 Google App Engine 上注册一个帐户(如果您已经有了 Gmail 的 Google 帐户,那么您可以跳过以下步骤使用它):

  1. 导航到(参见图 6-1 )。

    9781430240624_Fig06-01.jpg

    图 6-1T14 .谷歌应用引擎主页

  2. 点击注册链接。出现提示时,使用您的 Gmail 帐户登录。然后你将被带到你的应用列表(见图 6-2 )。

    9781430240624_Fig06-02.jpg

    图 6-2T13 .应用列表

  3. 单击创建应用按钮。下一页允许您选择有关您的应用的详细信息。(参见图 6-3 )。由于您的应用将公开可见,Google 为您提供了一个子域。appspot.com。这个子域池在应用引擎开发者的整个用户群中共享;因此,在某些情况下,您可能收不到您想要的应用名称。例如,你不太可能收到登录域名 1 子域名,因为我已经注册了。您可以通过单击“检查可用性”按钮来检查子域的可用性。

    9781430240624_Fig06-03.jpg

    图 6-3T14 .为您的应用命名

  4. 填写你想要的应用的子域;应该是类似 < 你的名字>ogindemo 1. appspot . com(见图 6-3 )。给你的应用一个标题,比如登录演示 1。保持其余选项不变,然后单击 Create Application。

  5. 如果一切顺利,您将会看到一个类似于图 6-4 的页面,告诉您您的应用已经成功创建。接下来,您可以通过单击“dashboard”链接来查看您的应用的状态。你的应用还没有做任何事情,所以统计数据仍然是空的(见图 6-5 )。

9781430240624_Fig06-04.jpg

图 6-4 。成功创建应用 ??

9781430240624_Fig06-05.jpg

图 6-5 。应用仪表板

接下来,您必须下载用于 Google App Engine 的 SDK,以便在将应用发布到 Google App Engine 服务器之前,您可以在本地计算机上编写、运行和调试应用。我在大部分开发中使用 Eclipse,我将概述下载 SDK 并将其直接与 Eclipse 集成所需的步骤。此外,由于我们正在覆盖 Android,我们将坚持 Java SDK 的应用引擎。

你可以在以下网址找到如何安装 Eclipse 的 Google Apps 插件的详细说明:code.google.com/eclipse/docs/getting_started.html。即使最终的 URL 发生了变化,您也应该能够通过导航到基本 URL,即 code.google.com/eclipse 的,到达文档部分。

我们还不打算写任何后端代码。首先,让我们编写一个存根应用,我们可以从它开始并在其上进行构建。在您的 Eclipse IDE 中,通过转到 FileimageNewimageWeb Application Project 来创建一个新的 Google App Engine 项目。将项目名填写为 LoginDemo,将包名填写为 net.zenconsult.gapps.logindemo,取消选中使用 Google Web Toolkit 旁边的复选框(参见图 6-6 )。完成后,点按“完成”。您将最终得到一个名为 LoginDemo 的项目;在命名的包中,您会发现一个名为的文件。该文件包含清单 6-2 中的代码。目前,它没有什么特别的。代码等待一个 HTTP GET 请求,然后用纯文本进行响应:“Hello,world。”

9781430240624_Fig06-06.jpg

图 6-6 创建一个新的谷歌应用引擎项目

清单 6-2 。 默认存根应用包 ,net . Zen consult . gapps . logindemo

import java.io.IOException;
import javax.servlet.http.*;

@SuppressWarnings("serial")
public class LoginDemoServlet extends HttpServlet {
  public void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws IOException {
            resp.setContentType("text/plain");
            resp.getWriter().println("Hello, world");
  }

}

让我们将这个应用部署到 Google App Engine,看看我们能否通过 web 浏览器访问它。要部署应用,在 Eclipse 包管理器中右键单击它,然后单击 Google image Deploy to App Engine。

然后会提示您选择您在 Google 网站上创建的远程应用的名称。在应用 ID 字段中输入您创建的名称(参见图 6-7 )并点击确定。在下一个窗口中,单击 Deploy 将您的应用上传到 Google(参见图 6-8 )。

9781430240624_Fig06-07.jpg

图 6-7 。选择远程应用的名称

9781430240624_Fig06-08.jpg

图 6-8 。将应用部署到 Google

成功部署应用后,您可以通过导航到创建应用时选择的 URL(<yourname>log in demo 1 . appspot . com)来检查应用。在我的例子中,当我导航到 logindemo1.appspot.com 的,时,我会看到“你好,世界”响应消息(参见图 6-9 )。

9781430240624_Fig06-09.jpg

图 6-9 。访问登录 servlet

我们现在有了自己的工作 web 应用,可以随心所欲地使用它。你可能已经注意到设置一个 Google App Engine 应用是多么的方便。这无疑节省了我们构建服务器、安装操作系统、安装服务器软件和配置服务器的时间和精力。让我们来看一些与 web 应用相关的理论。

HTML、网络应用和网络服务

任何 web 开发人员都知道 HTML 是什么。它是任何现代网站的基本组成部分之一。HTML(超文本标记语言)始于 1991 年的一份草稿;这是一种非常简单的语言,可以用来创建基本的网页。快进到 2008 年,HTML 版本 5 的草案发布了。纯 HTML 页面 被称为静态页面。换句话说,它们呈现在最终用户的浏览器上,并保持在那里,直到用户导航到另一个页面。

一个网络应用 是终端用户通过网络 — 访问的一个软件,就像 HTML 页面一样。然而,web 应用比普通的 HTML 包含更多的动态元素。例如,现代网络应用有许多服务器端语言。这些语言(例如 PHP、JSP 和 ASP)在运行时根据最终用户的输入动态生成静态 HTML。web 应用安装在 web 服务器上,并托管在最终用户可以通过网络(如互联网)访问的硬件上。服务器端应用框架负责呈现用户界面、任何应用逻辑(例如,搜索、计算或任何其他过程)以及数据存储或检索功能。最终用户所要做的就是带着他最喜欢的网络浏览器出现在聚会上。换句话说,因为所有复杂的处理都发生在后端或服务器端,所以更薄、更轻的 web 浏览器只不过是一种与用户界面交互的机制。

网络应用为开发者提供了许多优势 并且是当今网络生活中无处不在的一部分。它们最大的优势之一是能够向服务器推出更新或补丁,而不必担心更新成百上千的客户端。web 应用的另一大优势是,最终用户只需要一个瘦客户端——web 浏览器——仅此而已。因此,您不仅可以接触到来自个人计算群体的大量用户,还可以接触到移动计算群体。

一个 web 服务 类似于一个 web 应用,因为它可以通过网络远程访问。它的相似之处还在于它也运行某种服务器软件。然而,主要的区别是用户不能交互地访问服务。在大多数情况下,web 服务与其他客户端或服务器应用进行交互。在大多数情况下,web 服务能够描述它提供的服务以及其他应用如何访问它们。它使用 Web 服务描述语言(WSDL)文件来完成这项工作。其他应用可以通过处理发布的 WSDL 文件来了解如何使用 web 服务。通常,web 服务使用特定的 XML 格式来交换信息。其中一个流行的协议是 SOAP(简单对象访问协议)。SOAP 由基于特定应用的各种 XML 有效载荷组成。清单 6-3 显示了一个 SOAP 消息的例子。

清单 6-3 。 一个 SOAP 消息的例子(由维基百科提供)

POST /InStock HTTP/1.1
Host:[www.example.org](http://www.example.org)
Content-Type: application/soap  +  xml; charset = utf-8
Content-Length: 299
SOAPAction: "[`www.w3.org/2003/05/soap-envelope`](http://www.w3.org/2003/05/soap-envelope)"

“1.0”?>

>

>

IBM

web 服务的另一种工作方式是公开 RESTful API。REST 或表述性状态转移是一种架构,它使用底层的、无状态的客户端-服务器协议来公开 web 服务的端点。REST 的前提是使用一种简单得多的访问介质(如 HTTP ),对每个资源使用单独的 URIs,而不是依赖更复杂的协议,如 SOAP(使用单个 URI 和多个参数)。

你可以在罗伊·菲尔丁的学位论文www . ics . UCI . edu/∞Fielding/pubs/disserious/REST _ arch _ style . htm或维基百科的en . Wikipedia . org/wiki/representative _ state _ transfer上阅读更多关于 REST 的内容。尽管使用 RESTful web 服务很简单,但它仍然可以执行与使用 SOAP 的 web 服务相同的任务。以清单 6-3 中的 SOAP 为例。如果我们的 web 服务将它作为 RESTful API 公开给我们,那么我们会做这样的事情:

 [`www.example.com/stocks/price/IBM`](http://www.example.com/stocks/price/IBM) 

请注意,这是请求的范围。它可以作为一个简单的 HTTP GET 请求发送给服务器,然后服务器可以做出响应。有时,服务器可以用几种不同的表示形式返回数据。例如,如果我们向服务器请求 XML 输出,我们可以添加一个扩展 xml 。如果我们想要 JSON 格式的,我们可以添加一个 json 扩展名,如下所示:

 [`www.example.com/stocks/price/IBM.xml`](http://www.example.com/stocks/price/IBM.xml) 
 [`www.example.com/stocks/price/IBM.json`](http://www.example.com/stocks/price/IBM.json) 

现在是谈论 HTTP(超文本传输协议)的好时机。HTTP 是驱动 web 的协议。虽然超文本最初指的是普通的老式 HTML,但现在可以扩展到包括 XML(可扩展标记语言)。XML 遵循 HTTP 的规则,但是它包括可以使用的自定义 HTML 标签(或关键字)。HTTP 作为一种请求-响应协议。请求-响应循环发生在称为客户端和服务器的两方之间。客户端,或者说用户代理(一个网络浏览器),向网络服务器发出请求,网络服务器返回一个 HTML 或者 XML 的响应。大多数经验丰富的 web 开发人员有时也会期待与 XML 类似的格式,比如 JSON(JavaScript Object Notation)。

HTTP 请求被进一步细分为请求类型,或方法 。有几种方法,最常用的是 GET 和 POST 。 GET 请求用于检索数据, POST 请求用于提交数据。如果你正在填写注册表格,点击提交按钮会提示浏览器将你的数据发送到服务器。如果你回头看本章开头的清单 6-1 ,你会看到这一行:

HttpPost post = new HttpPost(" [`logindemo1.appspot.com/logindemo`](http://logindemo1.appspot.com/logindemo) ");

这是对特定 URL 的 HTTP POST 请求的创建。您可能知道,URL(统一资源定位器)是一种地址,它告诉用户代理从哪里检索特定的资源。资源可以是远程存储在服务器上的文件、文档或对象。HTTP 请求和响应都有相似的结构。两者都包含标题和内容区域。你可以在 www.w3.org 的 ?? 找到很多关于 HTTP 的附加信息。

Web 应用中的组件

Web 应用由不同的层组成。典型的 web 应用有三层(见图 6-10 ):表示层、逻辑层和数据层。根据应用的要求和复杂性,层数可能会增加。拥有多层应用有许多优势:其中之一是系统所有者可以独立于其他层替换或扩展硬件或服务器配置。考虑公司需要增加数据存储量的情况;IT 部门可以升级这一层,而无需对其他层进行重大更改。下一个优势是安全团队可以在每一层进行更精细的控制。每一层都有不同的功能,因此有不同的要求和相关的安全控制。多层应用允许所有者在各个层拥有更多锁定的控制,而不是留下空白,因为所有三层都在一个系统上。

因此,基于三层架构 ,一个 web 应用将包含一个呈现其数据的 web 服务器,一个处理所有数据交换请求的应用服务器,以及一个存储和检索数据的数据库服务器。

9781430240624_Fig06-10.jpg

图 6-10T3。一个三层的网络应用(由维基百科提供)

让我们通过一个例子来看看每一层是如何涉及的。

登录流程

客户端与服务器进行的标准用户身份验证会话如下所示:

  1. 客户端从 web 服务器**【Web 服务器/表示层】**请求登录页面。
  2. 客户端将凭证发送到 web 服务器**【Web 服务器/表示层】**。
  3. 应用服务器接收数据并检查其是否符合验证规则**【应用服务器/逻辑层】**。
  4. 如果数据是好的,那么应用服务器查询数据库服务器以发现是否存在匹配的凭证**【应用服务器/逻辑层】**。
  5. 数据库服务器响应应用服务器成功或失败**【数据库服务器/数据层】**。
  6. 应用服务器告诉 web 服务器向客户端提供其门户(如果凭证正确)或错误消息(如果凭证不匹配)【应用服务器/逻辑层】
  7. Web 服务器显示来自应用服务器**【Web 服务器/表示层】**的消息。

虽然这是一个简化的示例,但它确实说明了流程如何从外部移动到内部 — ,然后再返回。

Web App 技术

web 应用的每一层都可以使用多种技术。您可以从许多 web 服务器、应用框架、应用服务器、服务器端脚本语言和数据库服务器中进行选择。您的选择标准取决于多种因素,如应用要求、预算以及对您选择的技术的支持的可用性。

因为 Android 开发主要是在 Java 上完成的,所以我决定在我们的 web 应用中也坚持使用 Java。除了 Java,您还可以使用其他服务器端技术。这里列举了其中的一些:

类似地,根据您的需求,您可以将许多流行的数据库用于您的数据层应用。存在许多免费的和商业的数据库。这是您或您的应用架构师最初必须做出的又一个决定。这里有一个流行数据库的简短列表和一个 URL ,表明您可以在哪里了解更多关于它们的信息:

现在让我们花几分钟时间来完成我们的 web 应用,以便它支持基本的密码检查。请注意,我故意让这个例子非常简单。实际 web 应用的身份验证例程将更加复杂。查看清单 6-4 中的代码。

清单 6-4 。 新的凭证验证码

package net.zenconsult.gapps.logindemo;

import java.io.IOException;
import javax.servlet.http.*;

@SuppressWarnings("serial")
public class LoginDemoServlet extends HttpServlet {
  private String username = "sheran";
  private String password = "s3kr3tc0dez"; // Hardcoded here intended to
  // simulate a database fetch

  public void doGet(HttpServletRequest req, HttpServletResponse resp)
      throws IOException {
    resp.setContentType("text/plain");
  resp.getWriter().println("Hello, world");
  }

  public void doPost(HttpServletRequest req, HttpServletResponse resp)
      throws IOException {
    String user = req.getParameter("username"); // No user input validation
                                                    // here!
    String pass =   req.getParameter("password"); // No user input validation
                                                    // here!

    resp.setContentType("text/plain");
    if (user.equals(username) && pass.equals(password)) {
      resp.getWriter().println("Login success!!");
    } else {
      resp.getWriter().println("Login failed!!");
      }

    }
}

下一步是发布您的代码,就像您第一次设置 Google App Engine 帐户时所做的那样,然后创建一个新的处理身份验证的 Android 项目(项目结构见图 6-11 )。清单 6-5、 6-6、 6-7、和 6-8 分别包含了登录、登录民主客户端 1 活动、字符串. xml 和 main.xml 文件的源代码。确保将这一行添加到您的 AndroidManifest.xml 文件中,因为您将需要访问互联网来访问您的 Google App Engine 应用:

<uses-permission Android:name =【Android . permission . internet】>

9781430240624_Fig06-11.jpg

图 6-11T3。项目结构

清单 6-5 。 登录类

package net.zenconsult.android.examples;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;

import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;

import android.util.Log;

public class Login {
  private final String TAG = "HttpPost";
  private String username;
  private String password;

  public Login(String user, String pass) {
       username = user;
       password = pass;
  }

  public HttpResponse execute() {
       Log.i(TAG, "Execute Called");
       HttpClient client = new DefaultHttpClient();
       HttpPost post = new HttpPost("[`logindemo1.appspot.com/logindemo`](http://logindemo1.appspot.com/logindemo)");
       HttpResponse response = null;

       // Post data with number of parameters
       List< NameValuePair  >       nvPairs = new ArrayList  <  NameValuePair  >  (2);
       nvPairs.add(new BasicNameValuePair("username", username));
       nvPairs.add(new BasicNameValuePair("password", password));

       // Add post data to http post
  try {
            UrlEncodedFormEntity params = new UrlEncodedFormEntity(nvPairs);
            post.setEntity(params);
            response = client.execute(post);
            Log.i(TAG, "After client.execute()");

       } catch (UnsupportedEncodingException e) {
       Log.e(TAG, "Unsupported Encoding used");
       } catch (ClientProtocolException e) {
            Log.e(TAG, "Client Protocol Exception");
       } catch (IOException e) {
            Log.e(TAG, "IOException in HttpPost");
  }
  return response;
  }

}

6-5 中列出的代码包含登录程序。类构造函数 Login 有两个参数,分别是用户名和密码。 execute() 方法 将使用这些参数向服务器发出 HTTP POST 请求。

***清单 6-6 。***logindemoclient 1 活动类

package net.zenconsult.android.examples;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;

import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.EditText;

public class LoginDemoClient1Activity extends Activity implements
  OnClickListener {
  private final String TAG = "LoginDemo1";
  private HttpResponse response;
  private Login login;

  /** Called when the activity is first created. */
  @Override
  protected void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.main);

      Button button =     (Button) findViewById(R.id.login);
      button.setOnClickListener(this);

  }

  @Override
  public void onClick(View v) {
      String u = ((EditText) findViewById(R.id.username)).toString();
      String p = ((EditText) findViewById(R.id.password)).toString();

      login = new Login(u, p);

      String msg = "";
      EditText text =         (EditText) findViewById(R.id.editText1);
  text.setText(msg);

  response = login.execute();
      Log.i(TAG, "After login.execute()");

      if (response ! = null) {
  if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
      try {
  BufferedReader reader =     new BufferedReader(
  new InputStreamReader(response.getEntity()
  .getContent()));
  StringBuilder sb =     new StringBuilder();
  String line;
      while ((line = reader.readLine()) ! = null) {
  sb.append(line);
  }
  msg = sb.toString();
  } catch (IOException e) {
  Log.e(TAG, "IO Exception in reading from stream.");
      }

  } else {
      msg = "Status code other than HTTP 200 received";
  }
  } else {
  msg = "Response is null";
  }
      text.setText(msg);
  }
}

6-6 中列出的代码是一个标准的 Android 活动。这可以被认为是应用的入口或起点。

***清单 6-7 。***strings . XML 文件

<?xml version = "1.0" encoding = "utf-8"?>
<resources>
  <string name = "hello"  >  Web Application response:</string>
  <string name = "app_name"  >  LoginDemoClient1</string>
  <string name = "username"  >  Username</string>
  <string name = "password"  >  Password</string>
  <string name = "login"  >  Login</string>
</resources>

***清单 6-8 。***main . XML 文件

<?xml version = "1.0" encoding = "utf-8"?>
<LinearLayout xmlns:android = " [`schemas.android.com/apk/res/android`](http://schemas.android.com/apk/res/android) "
  android:orientation = "vertical"
  android:layout_width = "fill_parent"
  android:layout_height = "fill_parent"
  android:weightSum = "1">
  <TextView android:textAppearance = "?android:attr/textAppearanceLarge"
  android:id = "@  +  id/textView1" android:layout_height = "wrap_content"
  android:layout_width = "wrap_content" android:text = "@string/username">
  </TextView>
<EditText android:layout_height = "wrap_content"
  android:layout_width = "match_parent" android:id = "@  +  id/username">
</EditText>
<TextView android:textAppearance = "?android:attr/textAppearanceLarge"
  android:id = "@  +  id/textView2" android:layout_height = "wrap_content"
  android:layout_width = "wrap_content" android:text = "@string/password">
</TextView>
<EditText android:layout_height = "wrap_content"
  android:layout_width = "match_parent" android:inputType = "textPassword"
  android:id = "@  +  id/password">
</EditText>
<Button android:text = "@string/login" android:layout_height = "wrap_content"
  android:layout_width = "166dp" android:id = "@  +  id/login">
</Button>
<TextView android:text = "@string/hello" android:layout_height = "wrap_content"
  android:layout_width = "fill_parent">
</TextView>
<EditText android:id = "@  +  id/editText1" android:layout_height = "wrap_content"
  android:layout_width = "match_parent" android:inputType = "textMultiLine"
  android:layout_weight = "0.13">
  <requestFocus  >  </requestFocus>
</EditText>
</LinearLayout>

strings.xml 和 main 。 xml 文件分别包含我们定义的字符串常量集和应用的图形布局。

运行您的应用并输入不同的用户名和密码。您应该会看到两条不同的响应消息,一条表示成功,另一条表示密码失败(参见图 6-12 )。就是这样!您已经完成了移动登录客户端和服务器的编写。接下来,我们将讨论 web 上的安全性,以及在您的 web 应用中可能会遇到的各种攻击。

9781430240624_Fig06-12.jpg

图 6-12T3。登录失败

OWASP 和网页攻击

开放 web 应用安全项目(OWASP)【www.owasp.org】是一个为测试和保护 Web 应用提供大量知识、技术和指南的组织。OWASP 成立于 2001 年 12 月,并于 2004 年获得美国非营利慈善机构地位。它的核心目标是"成为一个蓬勃发展的全球社区,推动全球软件安全性的可见性和发展。“这是了解和修复 web 应用安全性的绝佳资源。

OWASP 十大项目自 2004 年以来一直是 OWASP 基金会 的子项目。在半定期的基础上,OWASP 十大漏洞列出了十个最重要的应用安全漏洞。这些漏洞被列为项目成员和全球安全专家在 web 应用中所经历的广泛共识。十大清单被大量商业组织使用和采纳,它已经成为 web 应用安全的事实标准。

在这本书出版的时候,2010 年 OWASP 十大仍然是最近更新的名单 。这里可以找到:【www.owasp.org/index.php/T…

下面列出了 2010 年 OWASP 十大主题:

  • A1:注射
  • A2:跨站点脚本(XSS)
  • A3:不完整的认证和会话管理
  • A4:不安全的直接对象引用
  • A5:跨站点请求伪造(CSRF)
  • A6:安全错误配置
  • A7:不安全的加密存储
  • A8:无法限制 URL 访问
  • A9:传输层保护不足
  • A10:未经验证的重定向和转发

较新的 OWASP 项目之一是移动十大,它是 OWASP 移动安全项目 的一部分。该项目仍在开发中,在撰写本文时还没有发布最终的清单。然而,网站上有一个实用技巧的列表,将证明对你这个移动开发者有巨大的帮助。本章涵盖的大多数主题都与移动十大共享许多技术和原则。以下是该列表涵盖的主题:

  • 识别和保护移动设备上的敏感数据。
  • 在设备上安全地处理密码凭证。
  • 确保敏感数据在传输过程中受到保护。
  • 正确实施用户认证/授权和会话管理。
  • 保持后端 API(服务)和平台(服务器)的安全。
  • 安全地执行与第三方服务/应用的数据集成。
  • 特别注意收集和存储同意收集和使用用户数据。
  • 实施控制以防止对付费资源(如钱包、短信和电话)的未授权访问。
  • 确保移动应用的安全分发/供应。
  • 仔细检查代码的任何运行时解释是否有错误。

认证技术

现在,让我们继续讨论保护“传输中的数据”的主题。我希望你对 web 应用的幕后工作有一个公平的理解,这就是为什么我在本章中讨论了与 web 应用相关的主题。如果你致力于成为一名移动应用开发人员,那么看看你的应用如何与你想与之交流的 web 应用进行通信是很有趣的。更好地理解应用还可以帮助您提高安全性和性能。如果像我一样,您从头到尾都在编写代码,包括 web 应用开发,那么您可能已经熟悉了我将要讨论的主题。不管怎样,既然您已经对 web 应用和安全性有了一个简短的回顾,那么让我们继续手头的主要任务。

身份验证是需要与远程 web 应用交互的移动应用的一个重要特性。几乎所有当今的应用都依赖某种形式的用户名和密码或 PIN 组合来授权对其数据的访问。用户名和密码存储在服务器上,每当最终用户希望通过应用进行身份验证时,就会进行比较。如果你重新看一下清单 6-1,你会发现我们正在这么做。以下几行包含 web 应用的用户名和密码:

nvPairs.add(new BasicNameValuePair("username", "sheran"));
nvPairs.add(new BasicNameValuePair("password", "s3kretc0dez"));

在这种情况下,信息是硬编码的,但它可以很容易地存储在设备上(当然是加密的!)并在用户想要登录时检索。但是如果我们的流量在传输过程中被拦截了呢?“啊哈!但是我们有 SSL!”你说。这是真的,但是我们似乎没有在我们的例子中使用它,因为我们的 POST 请求发送到一个非 SSL/TLS 端口:

HttpPost post = new HttpPost(" [`logindemo1.appspot.com/logindemo`](http://logindemo1.appspot.com/logindemo) ");

好吧,那是个卑鄙的手段。但是我们认真考虑一下,我们的 SSL 流量被攻破了。窃听我们与 web 应用对话的攻击者现在可以访问我们的凭据。她现在要做的就是直接在完整的网络应用或另一个移动设备上使用它们。如果她这样做,她将完全控制我们的用户资料。如果这是一个社交网站,那么我们可能不会太在意;然而,如果这是我们的网上银行应用,那么我们会非常担心。

到目前为止,我们知道在进行远程身份认证时面临的风险。尽管我们的数据可能会通过安全通道,但仍然容易受到攻击。而且不一定是像 DigiNotar 事件那样的严重攻击,攻击者可以颁发她自己的证书。例如,攻击可能像 SSL 中间人攻击一样平淡无奇。

因为我不止一次提到 DigiNotar 和不信任 SSL,所以我认为我概述一下我的理由是公平的。

你不能总是相信 SSL 。一般来说,最终用户认为 SSL 意味着他们是安全的。浏览器上的挂锁图标和地址栏变成绿色表示您正在浏览一个安全的网站。然而,这不一定是真的。我想花点时间回顾一下 SSL 的一些概念。

SSL(安全套接字层)是一种传输协议,它对两台计算机之间传输的数据进行加密。一个窃听者不可能不经过一番努力就截获加密数据——。因此,SSL 确保数据在客户端和服务器计算机之间保持私密。SSL 已经过时了。大多数人把客户端和服务器之间的加密 HTTP 数据传输称为 SSL 但实际上,较新的协议是 TLS(传输层安全)。SSL 和 TLS 不可或缺的一部分是 X.509 证书。X.509 是公钥基础设施(PKI)的标准,我在第五章中简单介绍过。通常,用户会将 X.509 服务器证书称为 SSL 证书。这是 SSL 的一个关键且非常重要的组件。图 6-13 显示了设置 SSL 会话的浏览器。

9781430240624_Fig06-13.jpg

图 6-13T3。设置 SSL/TLS 会话

TLS 和 SSL 结合使用加密技术来确保数据传输的安全性。现在让我们来看看这个会话设置。我不会给你外科手术的细节,因为你几乎永远不需要写自己的 TLS 协商算法。相反,本节将让您了解如何设置加密以及在 TLS 会话期间发生了什么。

首先,客户端或浏览器将联系 web 服务器并向其发送一些信息。该信息包含它可以支持的 TLS 版本的详细信息和加密算法列表。这些被称为密码套件 ,它们包含支持各种任务的算法,如密钥交换、认证和批量密码。

接下来,服务器在选择了它支持的特定密码套件以及客户机和服务器都支持的最高通用 TLS 版本后做出响应。然后,服务器还会向客户端发送其 SSL 证书。

然后,客户端使用服务器的公钥加密并交换一个预主密钥,一个生成主密钥的密钥。

交换预主密钥后,客户端和服务器将使用随机值和预主密钥来生成最终的主密钥。这个主密钥存储在客户端和服务器上。

然后,服务器和客户端切换到加密所有来回发送的数据。使用选定的密码套件,并在两端使用对称主密钥来加密和解密数据。图 6-14 显示了如果您能够捕获客户端和服务器之间的加密数据会话,您会看到什么。图 6-15 显示了使用 OpenSSL 查看时的握手和其他相关细节。只要看一眼就会立即告诉您,绝对没有可供攻击者使用的数据。那么,这对开发人员来说意味着什么呢?您应该使用 SSL,并且在客户机和服务器之间交换敏感数据时永远不用担心被窥探?我暂时不会接受你的回答。我们先来看几个细节,稍后再来回答你。

9781430240624_Fig06-14.jpg

图 6-14T3。SSL 会话的流量捕获

9781430240624_Fig06-15.jpg

图 6-15T3。使用 OpenSSL 的 s_client 选项查看 SSL 握手

SSL 与信任息息相关。实际上,X.509 是关于信任的。SSL 证书是根据特定标准颁发给个人或公司的。颁发机构,也称为 CA 或证书颁发机构,负责确定您是否是您所说的那个人。例如,你不能只申请一个 www.google.com 证书而不证明你以某种方式隶属于该公司,或者有能力代表该公司行事。这很重要,因为如果 CA 不检查这些凭证,那么任何人都可以申请 SSL 证书并将其安装在自己的 web 服务器上。

通过欺骗最终用户,让他们相信你的服务器是 google.com 服务器,你可以实施中间人攻击,拦截他的所有数据。我们很快会看到中间人攻击;但是首先,我想介绍另一个您可能知道的话题,即自签名证书。

注意CA 向客户端颁发 SSL 证书。颁发证书时,CA 还将使用自己的根证书对 SSL 证书进行签名。这个签名表明 CA 信任发布的 SSL 证书。浏览器可以通过首先查看 CA 签名并验证签名是否与根证书匹配来验证 SSL 证书。

世界各地都有许多知名的根 ca。通常,CA 根证书打包在您的 web 浏览器中。这允许浏览器验证由不同 ca 颁发的 SSL 证书。

例如,假设您向 VeriSign 申请了您的域名、example.com 的证书。VeriSign 首先确定您是该域的正确所有者,然后为您的 web 服务器颁发证书。它用自己的根证书签署该证书。收到 SSL 证书后,您可以将其安装在 web 服务器上并建立您的网站。现在当我访问您的网站时,我的浏览器首先查看您的 SSL 证书,然后尝试验证您的证书是否确实是由受信任的 CA 颁发的。为此,我的浏览器将查看其可信根证书的内部存储,以确定 VeriSign 根证书的签名是否与您的证书上的签名匹配。如果是的话,我可以继续浏览你的网站。但是,如果难以验证您的证书,我的浏览器会警告我无法验证证书。

请注意,在给证书开绿灯之前,您的浏览器将验证关于证书的许多其他细节。

自签名证书

在一些项目的开发和测试阶段,开发人员有时会在他们的网站上使用自签名证书。这种类型的证书在所有方面都与 CA 颁发的 SSL 证书相同。但是,主要的区别在于该证书上的签名不是来自可信的 CA。相反,开发人员自己签署证书。当浏览器使用自签名 SSL 证书连接到站点时,它无法验证谁签署了证书。这是因为签名者没有列在浏览器的内部可信证书库中。然后浏览器会向用户发出类似于图 6-16 中所示的警告。

9781430240624_Fig06-16.jpg

图 6-16T3。不可信或自签名证书的警告

发生在浏览器上的验证阶段非常重要。它的存在使得攻击者不能简单地给自己颁发一个属于的证书来欺骗用户。如果浏览器无法验证 SSL 证书,它将始终提醒用户。

中间人攻击

中间人(MitM) 攻击是一种攻击者可以窃听双方之间的网络流量或数据流动的方法。攻击者将自己定位成能够拦截来自发送方和接收方的流量,有效地将自己置于两者之间(见图 6-17 )。在这个位置上,他能够在双方之间截取和传递信息。如果执行正确,会话两端的用户都不会知道攻击者在中继和拦截他们的流量。

9781430240624_Fig06-17.jpg

图 6-17T3。艾丽丝和鲍勃中间的马洛里(维基百科提供)

下面是一个 MitM 攻击的例子,使用图 6-17 作为参考 ??:

Alice "Hi Bob, it's Alice. Give me your key"--> Mallory Bob
Alice Mallory "Hi Bob, it's Alice. Give me your key"--> Bob
Alice Mallory <--[Bob's_key] Bob
Alice <--[Mallory's_key] Mallory Bob
Alice "Meet me at the bus stop!"[encrypted with Mallory's key]--> Mallory Bob
Alice Mallory "Meet me in the windowless van at 22nd Ave!"encrypted with Bob's![image
 key]--> Bob

大多数时候,我们看到的攻击都集中在自签名证书上,或者诱骗浏览器相信攻击者拥有有效的证书。直到最近,攻击者对 CA 安全知之甚少,涉及 CA 的事件也少得多。不管怎么说,直到 2011 年 6 月之前都是如此。

理论上,攻击 CA 以获取合法签名的可信 SSL 证书也是一种选择。没有多少攻击者会考虑这一点,因为他们显然希望 CAs 具有高度的安全性。正确错了!2011 年 6 月,一个名为 DigiNotar 的 CA 遭到攻击。攻击者给自己颁发了 500 多个由 DigiNotar 签名的欺诈 SSL 证书。作为一个可信的 CA,DigiNotar 在所有现代浏览器中都有根证书。这意味着攻击者拥有合法的 SSL 证书,可以用来执行 MitM 攻击。由于浏览器已经信任 DigiNotar 根证书,它们将总是验证这些流氓 SSL 证书,最终用户永远不会知道攻击者正在拦截她的数据。

为什么会这样?DigiNotar 的基础设施安全控制非常松散。攻击者能够远程破坏其服务器,并访问负责颁发合法证书的系统。在这之后,对于攻击者来说,随时为自己颁发证书是一个相对简单的任务。一些有流氓证书的比较著名的网站 包括:

  • *.google.com (指【google.com】的任何子域,包括【mail.google.com】【docs.google.com】【plus.google.com】等等)
  • *.android.com
  • *.microsoft.com
  • *.mozilla.org
  • *.wordpress.org
  • www.facebook.com
  • www.mossad.gov.il
  • www.sis.gov.uk

所有的网页浏览器开发者都将 DigiNotar 的根证书列入黑名单,DigiNotar 开始系统地撤销所有的流氓证书。不幸的是,当这一切发生的时候,DigiNotar 已经失去了全球成千上万用户的信任。该公司于 2011 年 9 月宣布破产。

如果这么大的 CA 可以遭受这么大的安全漏洞,危及数百个 SSL 证书,那么我们真的可以一直依赖 SSL 吗?事实上,我们可以。像 DigiNotar 这样的事件很少发生,所以我会选择信任 SSL。然而,我也会选择在我的移动应用和服务器之间部署我自己的数据加密层。然后,如果 SSL 层被以任何方式破坏,攻击者将有另一层加密要处理。在大多数情况下,这一额外的层将作为一种威慑,攻击者可能会离开您的应用。

有没有一种方法可以防止攻击者在通过 SSL 传输时窥探我们的凭据?确实是的!让我们来看两种方法,即使我们的安全传输通道出现故障,我们也可以防止我们的凭据被破坏。一个是 OAuth ,一个是挑战/响应。

听觉

OAuth 协议允许被称为消费者 的第三方网站或应用使用被称为服务供应器 的网络应用上的最终用户数据。最终用户对他可以授予这些第三方的访问权限拥有最终控制权,并且这样做时不必泄露或存储他现有的 web 应用凭证。

以 Picasa 网络相册为例;照片编辑应用 Picnik(www.picnik.com)允许最终用户编辑他们的照片。Picnik 还允许终端用户从 Picasa 和 Flickr 等其他网站导入内容。在 OAuth 之前,用户必须登录 Picnik,还要输入他的 Picasa 或 Flickr 用户名和密码,这样 Picnik 就可以开始从这些网站导入照片。这种方法的问题是,现在用户已经用 Picnik 保存或使用了他的凭证。他的曝光度增加了,因为他在 Picasa Picnik 保存了自己的凭证。

如果用 OAuth 重现相同的场景,那么用户就不必在 Picnik 站点上再次输入凭证。相反,Picnik(消费者)会将他重定向到他的 Picasa(服务供应器)网站(见图 6-18 )并要求他允许或拒绝访问 Picasa 上存储的照片(见图 6-19 )。这样,用户的凭证更安全。

9781430240624_Fig06-18.jpg

图 6-18T3。Picnik 请求连接到 Picasa,这样它就可以请求一个访问令牌

9781430240624_Fig06-19.jpg

图 6-19T3。Picasa 请求授权让 Picnik 查看一些照片

OAuth 通过使用请求令牌来工作。想要访问 web 应用中的数据的站点需要从该应用获得一个令牌,然后才能开始访问这些数据。

让我们先来看看 OAuth 是如何为 Picasa 网络相册工作的。例如,假设您编写了一个列出用户 Picasa 相册的 Android 应用。您的 Android 应用需要访问用户的 Picasa 网络相册才能做到这一点。在这种情况下,参与者是您的 Android 应用(消费者)、Picasa(服务供应器)和您的最终用户。

OAuth 要求您首先在进行身份验证的站点上注册您的消费者应用。这是必要的,因为您将收到一个需要在代码中使用的应用标识符。要注册您的应用,您必须访问【http://code.google.com/apis/console】(参见图 6-20 ),创建一个项目,并创建一个 OAuth 客户端 ID(参见图 6-21 、 6-22 、 6-23 和 6-24 )。

9781430240624_Fig06-20.jpg

图 6-20T3。在 Google APIs 上创建一个新项目

9781430240624_Fig06-21.jpg

图 6-21T3。创建新的客户端 ID

9781430240624_Fig06-22.jpg

图 6-22T3。填写您的申请详情

9781430240624_Fig06-23.jpg

图 6-23T3。选择您的申请类型

9781430240624_Fig06-24.jpg

图 6-24T3。您的客户端 ID 和客户端密码现在已创建

现在你已经得到了 OAuth 客户端 ID,让我们来看看 OAuth 应用的认证流程 (见图 6-25 )

9781430240624_Fig06-25.jpg

图 6-25T3。OAuth 认证流程(由 Google 提供)

OAuth 是一个包含三个主要交互方的多阶段流程。消费者是希望从服务提供者那里访问数据的应用,这只有在用户明确授权消费者的情况下才会发生。让我们详细回顾一下这些步骤:

当最终用户打开您的 Android 应用 时,会启动以下场景:

  1. 流程 A: 消费者应用(您的 Android 应用)向服务供应器(Picasa)请求令牌。
  2. 流程 B: Picasa 告诉您的应用将最终用户重定向到 Picasa 的网页。然后,您的应用会打开一个浏览器页面,将最终用户指引到特定的 URL。
  3. 流程 C: 最终用户在该屏幕中输入她的凭证。请记住,她正在登录服务供应器(Picasa)网站,并授权访问您的应用。她将凭据发送到网站,而不是存储在设备上的任何地方。
  4. 流程 D: 一旦 Picasa 确认最终用户输入了正确的用户名和密码,并授予了对您的应用的访问权限,它会回复一个响应,指示请求令牌是否已被授权。此时,您的应用必须检测到这种响应并采取相应的行动。假设授权被授予,您的应用现在有一个授权的请求令牌。
  5. 流程 E: 使用这个授权的请求令牌,你的应用向服务供应器发出另一个请求。
  6. 流程 F: 然后,服务提供者将请求令牌交换为访问令牌,并在响应中将其发送回去。
  7. 你的应用现在使用这个访问令牌来访问任何受保护的资源(在这个例子中是用户的 Picasa 相册),直到令牌过期。

您的应用现已成功访问 Picasa,无需存储最终用户的凭据。如果用户的手机遭到破坏,攻击者复制了所有应用数据,他将无法在您的应用数据中找到 Picasa 用户名和密码。这样,你就确保了你的应用不会不必要地泄露敏感数据。

我在这里使用 Picasa 只是作为一个参考框架。我们的最终目标是为我们的后端应用创建一个 OAuth 认证系统。因此,您的后端 web 应用 将成为 OAuth 服务提供者,而不是 Picasa 作为服务提供者。您的最终用户必须通过 web 浏览器登录到您的应用,并明确授权它访问资源。接下来,您的移动应用和后端 web 应用将使用请求和访问令牌进行通信。最重要的是,你的移动应用不会保存你的网络应用的用户名和密码。

为了说明这些概念,我为 Picasa 创建了一个示例应用。我将在第八章向你展示如何在你的 web 应用中实现 OAuth。

用密码术挑战/响应

保护您的最终用户凭证不通过 Internet 的第二种机制是使用挑战/响应技术。这种技术在许多方面与 OAuth 相似,因为没有凭证通过网络。相反,一方请求另一方挑战。然后,另一方将根据特别选择的算法和密码功能对随机信息进行加密。用于加密这些数据的密钥是用户密码。这个加密的数据被发送到质询方,然后质询方使用存储在其末端的密码对同一条信息进行加密。然后比较密文;如果匹配,则允许用户访问。学习这种技术的最好方法是通过一个实际的例子。与 OAuth 一样,我在第八章中包含了源代码和应用示例。

总结

在这一章中,我们重点讲述了如何将我们的数据从移动应用安全地传输到 web 应用。我们还介绍了如何使用成熟的协议和机制来保护传输中的数据。与此同时,我们看到,在某些情况下,我们无法信任协议本身。在这种情况下,我们会考虑一些选项,帮助我们保护最终用户的凭据不被窃取或拦截。

我们还讨论了涉及 web 应用安全性的主题。考虑到大多数移动应用以某种形式与 web 应用通信,了解这方面的技术如何工作总是有好处的。最后,我们查看了一些有助于我们保护 web 应用的有用资源,以及一些在传输过程中保护用户凭证的具体示例。

七、企业安全

一直以来,我们都是站在个人开发者的角度来看待移动应用。尽管我相信个人开发者或较小的开发公司远远超过企业开发者,但我认为关注企业开发者和他可能面临的独特挑战是有益的。你可能想跳过这一章,因为你不属于“企业开发者”的范畴;然而,我会敦促你考虑这一点:现在大多数企业都在考虑外包他们的开发工作。

对于一个企业来说,拥有一个内部的移动开发团队可能是没有意义的,除非这是公司的核心业务。我见过许多企业将开发工作外包给个人或小公司,这样他们就不用担心管理内部移动开发团队。

如果有一天,一家公司雇佣你为它开发一个移动应用,那么在你开始开发之前,你可能需要考虑几个领域。在大多数方面,你的目标群体比你向公众发布你的应用要小得多。

然而,重要的一点是,就企业而言,您可能要处理的不仅仅是个人信息的丢失。例如,在企业环境中,您处理机密信息(例如,商业秘密、公司财务信息或敏感的服务器凭证)的可能性比您处理向公众发布的应用的可能性要高得多。此外,您的应用可能会更容易成为攻击目标,因为目前许多攻击者认为移动平台由于安全性较低而“容易得手”。让我们首先来看一下企业应用与公开发布的应用的一些主要区别。

连通性

最近,从远程位置连接到企业环境已经变得司空见惯。远程办公、远程支持和外包都导致企业技术团队允许授权用户进入他们组织的网络。这并不意味着网络管理员只是让防火墙对远程登录和远程桌面敞开大门;入站连接受到某些安全控制。为了确保最安全的路由,组织通常会使用 VPN 或虚拟专用网络(见图 7-1 ),以允许远程用户加入其网络。

9781430240624_Fig07-01.jpg

图 7-1 。虚拟专用网(VPN)(由维基百科提供)

VPN 通常是网络管理员通常会在其边界网络设备上创建的附加逻辑或虚拟网络。该网络充当公共网络(如互联网)和企业的私有内部网络之间的桥梁。用户可以通过这个公共网络连接到 VPN,并使用企业的内部资源(包括文件服务器、内部应用等),就像他们实际连接到内部网络一样。

VPN 也逐渐进入移动领域。黑莓、iPhone 和 Android 等设备现在能够连接到企业网络并安全地传输数据。在为企业设计时,请记住这一点。很有可能企业网络管理员会告诉你需要使用 VPN 但是如果她没有提到,你应该提出这个话题。这里的目标不是让一个企业向互联网公开更多它不应该公开的内容。

如果出于某种原因,你遇到一个没有或没有使用 VPN 的组织,那么你可能想花一点时间讨论一下 VPN 的优点。如果这是绝对不行的,那么你将不得不对应用和服务器之间的数据进行加密。但是,请记住,这样做的成本很高,尤其是在有大量数据交换的情况下。在这种情况下,您可能还需要考虑数据压缩。在这一章的后面我会给你一个数据压缩的例子。所有这些都增加了处理器的使用,但是,你需要考虑到,在几乎所有的情况下,这将耗尽你的终端用户的设备电池。

企业应用

那么,我一直在谈论的这些企业应用是什么呢?请放心,他们不像独角兽那样神秘;他们确实存在。如果你没有太多的机会在企业中工作,那么你可能不会马上认出一个企业系统。有许多不同的类型,但这里我们将重点放在企业资源规划(ERP) 应用上,主要是因为它们往往涵盖了企业中广泛的用途。典型的 ERP 应用通常涵盖以下领域之一:

  • 供应链管理
  • 客户关系管理
  • 制造业
  • 人力资源
  • 财务和会计
  • 项目管理

你将不得不使用的 ERP 应用很可能是成熟的和完善的。作为新的开发人员,您还可能需要编写自己的应用来与现有系统一起工作。这可能有点令人沮丧,尤其是当这意味着您必须在移动应用的某些功能上做出妥协的时候。在我看来,解决这个问题的最好方法之一是采用和使用某种形式的移动中间件 。

移动中间件

与其让自己的移动应用与遗留的企业应用一起工作,还不如花些时间开发自己的移动中间件平台。简单地说,移动中间件平台在您的移动应用与企业系统的通信中充当中间人的角色。目标 是让您的移动应用能够处理企业应用中的数据,而不会影响操作系统功能或移动设备上可用的有限资源。

我曾经测试过一个银行手机应用的安全性。移动应用开发人员在与一个非常专有的、封闭的、文档记录不充分的应用集成时,遵循了使用移动中间件的思想。开发人员以屏幕翻译器的形式创建了一个移动中间件组件。本质上,这是一个基于服务器的应用,它将从银行应用 获取网站,挖掘或复制特定页面上的所有文本,然后将这些页面转换为移动格式的文本。

看一下图 7-2 。它展示了移动应用如何连接到一个中间件系统,该中间件系统抽象了遗留应用的数据和用户界面。在某些情况下,移动客户端可以通过移动浏览器直接访问遗留应用,但在这种情况下,它不会提供理想的用户体验。因此,通过与移动中间件接口,应用的通信基础设施可以标准化。与遗留应用的大多数交互 将在更强大的硬件上完成。

9781430240624_Fig07-02.jpg

图 7-2 。移动中间件示例

考虑到这一点,当我们决定开发企业移动应用时,我们需要确定我们将会遇到的一些关键场景。在这一章中,我着眼于开发企业移动应用时被证明是一个挑战的两个领域:数据库访问 和数据表示。在移动企业应用开发过程中,这些特定的领域被证明是一个挑战。让我们从数据库访问开始。

数据库访问

Android 支持可以用来访问数据库服务器的 javax.sql 和 java.sql 包。让我们从一个非常简单但不安全的示例应用 — 开始,向您展示这种方法的不足之处。接下来,我们将看看一些更好的技术。您可能会奇怪,为什么我要浪费您的时间来详细研究一个不安全的解决方案。重点是看它为什么没有安全感;只有当你明白它是多么的不安全,你才会充分体会到正确方法的好处。随意向前跳——后果自负!

该应用将连接到一个 MySQL 数据库,并从名为 apress 的表中读取数据。为了正确执行,Android 设备和数据库服务器应该位于同一个网络上。我将把数据库的设置和创建留给您。确保您设置了数据库服务器来监听公共 IP 地址。你可以通过编辑 MySQL 安装中的 my.cnf 文件来实现。清单 7-1 包含了数据库模式。确保首先创建名为 android 的数据库。创建表格后,向其中输入一些测试数据,这样当您使用 Android 应用连接到表格时就可以检索到它。

清单 7-1。 一条 MySQL SQL 语句创建一个 apress 表

CREATE TABLE `apress` (
   `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
   `name` varchar(50) NOT NULL DEFAULT '',
   `email` varchar(50) DEFAULT NULL,
   PRIMARY KEY (`id`)
) ENGINE = MyISAM AUTO_INCREMENT = 4 DEFAULT CHARSET = latin1;

现在让我们开始开发应用吧。创建一个名为 MySQLConnect 的新项目。在您的项目文件夹中,创建一个名为 lib 的新文件夹。现在从 www.mysql.com/products/co… MySQL Connector/J。接下来,解压存档并复制。jar 文件到您的 lib 目录。该文件应该类似于 MySQL-connector-Java-5 . 1 . 15-bin . jar。如果您正在使用 Eclipse 进行开发,那么您的项目布局将类似于图 7-3 中的布局。在我的布局中,你可以看到我有几个版本的 MySQL 连接器,但是我使用的是最新的版本。

9781430240624_Fig07-03.jpg

图 7-3 。MySQLConnect 项目结构

在这个例子中,我们创建了一个列表视图布局。这为我们从数据库中检索的数据提供了一个很好的全屏列表。因为列表视图将包含单独的项目,我们必须告诉 Android 每个项目是什么。为此,我们创建一个名为 list_item.xml 的新 XML 文件,包含清单 7-2 中的文本,然后将其保存在布局文件夹下,如图 7-3 所示。

***清单 7-2。***list _ item . XML 文件内容

<?xml version  =  *"1.0"*encoding  =  *"utf-8"*?>
<TextView xmlns:android  =  "http://schemas.android.com/apk/res/android"
   android:layout_width  =  *"fill_parent"*
   android:layout_height  =  *"fill_parent"*
   android:padding  =  *"10dp"*
   android:textSize  =  *"16sp"*>
</TextView>

这告诉 Android 每个列表项都是文本类型的,并给它一些关于文本填充和字体大小的详细信息。接下来是 MySQLConnectActivity.java 文件的代码(见清单 7-3 )。记下将主机 IP 地址、用户名和密码更改为您创建的内容。

***清单 7-3。【MySQLConnectActivity.java】***源代码

package net.zenconsult.android;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Enumeration;
import java.util.Hashtable;

import android.app.ListActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;

public class MySQLConnectActivity extends ListActivity {
   /** Called when the activity is first created. */
   @Override
   public void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);

      Connection conn  =  null;
      String host  =  "192.168.3.105";
      int port  =  3306;
      String db  =  "android";

      String user  =  "sheran";
      String pass  =  "P@ssw0rd";

      String url  =  "jdbc:mysql://"  +  host  +  ":"  +  port  +  "/"  +  db  +  "?user = "
      + user  +  "&password = "  +  pass;
      String sql  =  "SELECT * FROM apress";

      try {
         Class.forName("com.mysql.jdbc.Driver").newInstance();
         conn  =  DriverManager.getConnection(url);

         PreparedStatement stmt  =  conn.prepareStatement(sql);
         ResultSet rs  =  stmt.executeQuery();
         Hashtable  < String, String  >  details  =  new Hashtable  < String, String  > ();
         while (rs.next()) {
           details.put(rs.getString("name"), rs.getString("email"));
         }
         String[] names  =  new String[details.keySet().size()];
         int x  =  0;
         for (Enumeration  < String  >  e  =  details.keys(); e.hasMoreElements();) {
           names[x]  =  e.nextElement();
           x++;
         }
         conn.close();
         this.setListAdapter(new ArrayAdapter  < String  > (this,
           R.layout.list_item, names));

         ListView lv  =  getListView();
         lv.setTextFilterEnabled(true);

         lv.setOnItemClickListener(new OnItemClickListener() {
           public void onItemClick(AdapterView  < ?  >  parent, View view,
           int position, long id) {
             Toast.makeText(getApplicationContext(),
             ((TextView) view).getText(), Toast.LENGTH_SHORT).show();
            }
         });

         } catch (ClassNotFoundException e) {
           Log.e("MYSQL", "Class not found!");
         } catch (SQLException e) {
           Log.e("MYSQL", "SQL Exception "  +  e.getMessage());
         } catch (InstantiationException e) {
           Log.e("MYSQL", "Instantiation error "  +  e.getMessage());
         } catch (IllegalAccessException e) {
           // TODO Auto-generated catch block
           e.printStackTrace();
         }

   }
}

因为我们正在访问网络,你必须确保你的应用具有在 AndroidManifest.xml 文件中设置的 Android . permission . internet 权限。

保存您的项目并在您的 Android 模拟器上运行它。您的应用应该启动,连接到数据库,检索数据,并以类似于图 7-4 所示的全屏列表视图显示数据。

9781430240624_Fig07-04.jpg

图 7-4 。应用正确执行时的输出

正如你所看到的,即使我们能够直接从数据库中读取数据,除了用我们的应用打包大型 JDBC 驱动程序库之外,似乎还有很多繁琐的代码需要我们编写。

在某些情况下,如果你不得不连接到一个没有纯 JDBC 驱动的数据库,那么你就被困住了。如果您考虑安全问题,那么您需要考虑您的数据库服务器必须暴露在互联网或 VPN 上,因为移动设备和数据库服务器都应该能够相互通信。最后,您可以看到数据库凭证存储在应用中。

请看下面这段代码:

Connection conn  =  null;
   String host  =  "192.168.3.105";
   int port  =  3306;
   String db  =  "android";

   String user  =  "sheran";
   String pass  =  "P@ssw0rd";
   String url  =  "jdbc:mysql://"  +  host  +  ":"  +  port  +  "/"  +  db  +  "?user = "
    + user  +  "&password = "  +  pass;
   String sql  =  "SELECT * FROM apress";

以 String user 和 String pass 开始的行显示了数据库凭证是如何在应用中被硬编码的。如果手机遭到破坏,攻击者可以从你的应用数据中读取数据库凭据,并使用它们从另一台计算机连接,直接攻击你的数据库。

因此,在你的 Android 应用中使用原生 JDBC 连接并不是最好的方法。最好编写一个移动中间件模块,让 app 以更方便、更安全的方式访问数据。

我们如何改进数据库访问过程?HTTP 是最简单也可能是最成熟的请求/响应机制之一。通过使用 HTTP ,我们当然可以简化和提高我们的数据访问方法的安全性。Android 已经内置了非常强大的 HTTP 客户端;我们有 SSL 来保护我们的数据;而且,如果需要,我们可以为来回传输的数据添加额外的加密层。你可能会说使用 HTTP 是不需要动脑筋的,所以我们就这么做吧。

但是我们应该如何使用 HTTP 从数据库中请求数据呢?我们可以使用 web 服务从后端获取数据。我们可以使用 REST(表述性状态转移)进行通信,而不是制作非常复杂的 web 服务。公开 RESTful API 将极大地简化我们的移动应用请求数据的方式。考虑这个例子:

 [`192.168.3.105/apress/members`](https://192.168.3.105/apress/members) 

通过发出这个 get 请求,我们可以获取与之前在 MySQLConnect 示例中获取的数据集相同的数据集。使用 HTTP 请求获取数据肯定要简单得多。当然,下一步是检索数据。因为我们选择了 HTTP 作为我们的传输机制,所以我们必须使用 HTTP 友好的响应机制。这就给我们带来了数据表示的问题。我们将在下一节中讨论这一点。

我希望您正在构建自己的库集,以便以后重用。这是一个很好的练习。我有几个不同的库,它们是我在开发时为不同的任务创建的。我有一个处理数据库连接的库,一个处理数据编码和解码的库,还有许多我在构建应用时使用的其他小工具库。它们加快了我的开发周期,并且通常使一切保持一致的状态。我现在提出这一点是因为,如果您打算开始构建自己的定制移动中间件的旅程,那么如果您将它设计成可以插入到尽可能多的部署场景中,您会更好。从那里,您可以调整配置设置,这样您就可以快速启动并运行。

注意自定义库

开发你自己的库是一个很好的实践。对我来说,编写自己的库意味着我永远不会忘记几个月前完成的一个特定的实现。我可以简单地调用我的共享库函数,并毫不费力地集成它。

但是,请记住,您所有的外部库函数都应该非常简单。这些基本功能以后可以串联起来执行一个复杂的功能。因此,您可以构建自己的库,并完全加快开发时间。

假设您花费了大量的时间和精力为您的客户编写一个电子商务应用。项目完成后,可能没有明确要求保留源代码。然而,如果你遇到另一个客户希望你建立一个类似的电子商务商店,这可能对你很重要。如果您对自己在早期应用中编写的代码拥有无可争议的所有权,您就可以重用它,从而大大减少准备新应用所需的时间。

数据表示

解决了这个问题之后,让我们来谈谈数据表示。通过数据表示,我指的是您的移动应用如何从后端 web 应用接收数据。在我们的案例中,我们正试图将我们的移动应用接收和处理数据的方式标准化。目前最常见的数据表示格式是 XML(可扩展标记语言)和 JSON (JavaScript 对象表示法)。因此,让我们致力于编写移动应用框架来接收和处理这种类型的数据。关于 XML 和 JSON 的快速入门,请参考附录。选择这种类型的数据表示的另一个原因是,有许多第三方的开源库,您可以根据自己的目的使用或修改。

回到我们的 RESTful API 请求,让我们看看我们可能从移动中间件得到的以下两个潜在响应:

XML
<?xml version = "1.0" encoding = "UTF-8"?>
<apress>
   <users>
    <user name = "Sheran" email =  "sheranapress@gmail.com" />
      <user name = "Kevin" email = "kevin@example.com" />
      <user name = "Scott" email = "scottm@example.com" />
   </users>
</apress>
JSON
{
   users:{
    user:[
    {
    name:'Sheran',
    email:'sheranapress@gmail.com'
    },
    {
    name:'Kevin',
    email:'kevin@example.com'
     },
    {
    name:'Scott',
    email:'scottm@example.com'
     }
     ]
  }
}

好的一面是,您不需要编写这么多代码来读取 XML 和 JSON 表示。Android 包括解析这两种格式的库。让我们看一些源代码。再次创建一个新项目,并将其命名为 RESTFetch 。像前面的例子一样创建 list_item.xml 文件,然后将 Android . permission . internet 权限分配给应用。清单 7-4 包含应用的代码,它将发出请求,处理 XML 响应,并在列表中呈现结果。图 7-5 包含输出。

清单 7-4。 使用 RESTful API 获取数据并处理 XML 输出

packagenet.zenconsult.android;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.StringReader;
import java.net.URI;
import java.net.URISyntaxException;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;
import org.w3c.dom.Document;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import android.app.ListActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;

public class RESTFetchActivity extends ListActivity {
   @Override
   public voidonCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    BufferedReader in =  null;

    try{
    HttpClient client =  new DefaultHttpClient();
    HttpGet request =  new HttpGet();
    request.setURI(new URI("http://192.168.3.105/apress/members"));
    HttpResponse response =  client.execute(request);
    in =  new BufferedReader(new InputStreamReader(response.getEntity()
    .getContent()));
    StringBuffer sb =  new StringBuffer("");
    String line =  "";
    String newLine =  System.*getProperty*("line.separator");
    while ((line =  in.readLine()) ! =  null ) {
      sb.append(line  +  newLine);
    }
    in.close();

    Document doc =  null ;

    DocumentBuilderFactory dbf =  DocumentBuilderFactory.*newInstance*();

    DocumentBuilder db =  dbf.newDocumentBuilder();

    InputSource is =  new InputSource();
    is.setCharacterStream(new StringReader(sb.toString()));
    doc =  db.parse(is);

    NodeList nodes =  doc.getElementsByTagName("user");
    String[] names =  new String[nodes.getLength()];
    for (int k =  0; k  <  nodes.getLength(); ++k) {
      names[k] =  nodes.item(k).getAttributes().getNamedItem("name")
        .getNodeValue();
    }

    this .setListAdapter(new ArrayAdapter  < String  > (this ,
      R.layout.*list_item*, names));

    ListView lv =  getListView();
    lv.setTextFilterEnabled(true );

    lv.setOnItemClickListener(new OnItemClickListener() {
     public void onItemClick(AdapterView  < ?  >  parent, View view,
       int position,long id) {
        Toast.*makeText*(getApplicationContext(),
        ((TextView) view).getText(), Toast.*LENGTH_SHORT*)
        .show();
    }
    });

    }catch (IOException e) {
       Log.*e*("REST", "IOException "  +  e.getMessage());
    }catch (URISyntaxException e) {
       Log.*e*("REST", "Incorret URI Syntax "  +  e.getMessage());
    }catch (ParserConfigurationException e) {
       //TODO Auto-generated catch block
    e.printStackTrace();
    }catch (SAXException e) {
       //TODO Auto-generated catch block
       e.printStackTrace();
    }

   }
}

9781430240624_Fig07-05.jpg

图 7-5 。带有 XML 响应的 RESTful API 查询的输出

对于 JSON 请求/响应代码和输出,分别看一下清单 7-5 和图 7-6 。

清单 7-5。 使用 RESTful API 获取数据并处理 JSON 输出


package net.zenconsult.android;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URI;
import java.net.URISyntaxException;

import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import android.app.ListActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;

public class RESTJSONActivity extends ListActivity {
   @Override
   public void onCreate(Bundle savedInstanceState) {
    super .onCreate(savedInstanceState);
    BufferedReader in =  null ;
    try {
        HttpClient client =  new DefaultHttpClient();
        HttpGet request =  new HttpGet();
        request.setURI(new URI("http://192.168.3.105/apress/members.json"));
        HttpResponse response =  client.execute(request);
        in =  new BufferedReader(new InputStreamReader(response.getEntity()
        .getContent()));
        StringBuffer sb =  new StringBuffer("");
        String line =  "";
        while ((line =  in.readLine()) ! =  null ) {
           sb.append(line);
        }
        in.close();
        JSONObject users =  new JSONObject(sb.toString())
          .getJSONObject("users");
        JSONArray jArray =  users.getJSONArray("user");
        String[] names =  new String[jArray.length()];
        for (int i =  0; i  <  jArray.length(); i++) {
           JSONObject jsonObject =  jArray.getJSONObject(i);
           names[i] =  jsonObject.getString("name");
        }
    this .setListAdapter(new ArrayAdapter  < String  > (this ,
        R.layout.*list_item*, names));

      ListView lv =  getListView();
      lv.setTextFilterEnabled(true );
      lv.setOnItemClickListener(new OnItemClickListener() {
      public void onItemClick(AdapterView  < ?  >  parent, View view,
      int position,long id) {
         Toast.*makeText*(getApplicationContext(),
        ((TextView) view).getText(), Toast.*LENGTH_SHORT*)
        .show();
       }
     });
        }catch (IOException e) {
        Log.*e*("RESTJSON", "IOException "  +  e.getMessage());
        }catch (URISyntaxException e) {
        Log.*e*("RESTJSON", "Incorret URI Syntax "  +  e.getMessage());
        }catch (JSONException e) {
        //TODO Auto-generated catch block
        e.printStackTrace();
        }
   }
}

9781430240624_Fig07-06.jpg

图 7-6 。带有 JSON 响应的 RESTful API 查询的输出

如果需要,可以将 XML 和 JSON 示例合并到一个类文件中。为了区分响应类型,通常可以在成员请求后附加一个文件扩展名。因此,对于 XML 响应,调用192 . 168 . 3 . 105/a press/members . XML; 并且,对于 JSON 响应,调用192 . 168 . 3 . 105/a press/members . JSON。同样,我们可以修改我们的示例,以便我们分析响应数据来自动发现结构。这将使我们能够根据某些关键字提取数据,而不管它们出现在哪里。然而,在大多数情况下,在代码中定义你的数据结构是无害的,因为毕竟,你的移动应用只会与你的移动中间件对话。

说到移动中间件,生成 XML 和 JSON 响应的服务器端代码到底在哪里?目前,这样的代码超出了本书的范围。但是为了让您更好地理解如何实现这种类型的移动中间件,请看附录中一个非常基本的例子,它也共享了部署说明。

摘要

如果让您开发一个与遗留企业系统一起工作的移动应用,我们快速地看了一下您将面临的两个问题。毫无疑问,当您涉足移动企业应用开发领域时,您可能会遇到不同的挑战。几乎在所有情况下,您都可以通过在移动中间件中构建翻译或桥接模块来克服这些问题。

就安全性而言,在本章的开始,我们讨论了向公众开放企业环境是一个坏主意。最好的方法是通过使用中间件来减少企业系统的暴露。我们决定使用 HTTP,不仅因为它的简单,还因为我们不需要做任何神奇的事情来保护它。可以应用与 SSL 相同的安全控制,而无需更改我们的任何代码。当然,我们也可以为我们的数据创建额外的加密和压缩层。

八、概念实战:第二部分

在这一章中,就像在第四章中一样,我们将更仔细地看看实现我们已经讨论过的一些理论概念的源代码和应用。这会让你对如何在实践中应用它们有更好的感觉。本章的代码示例将着重于设备上的安全认证和保护密码。回想一下,我们已经讨论了两种登录到后端应用而不在设备上存储凭证的机制。在这里,我们将探索与此相关的更详细的源代码。

oath〔??〕

让我们重温一下第六章中的 OAuth 登录示例。我们讨论了开发一个应用,该应用将与 Google Picasa 网络相册交互,以读取特定用户的相册列表。本章中的代码会做到这一点。查看这本书在 www.apress.comT3 的网站上的最新代码。首先,我们来看看图 8-1 中我们的项目结构。您将看到几个源文件。我们将讨论每个源文件的关键功能。

9781430240624_Fig08-01.jpg

图 8-1 。OAuth 示例的项目结构

正在检索令牌

你可以在图 8-1 中看到 OAuth 示例项目的结构。让我们从应用的入口点开始,它是 OAuthPicasaActivity.java 的,如清单 8-1 所示。

清单 8-1。 申请入口点

**package** net.zenconsult.android;

**import** android.app.ListActivity;
**import** android.content.Intent;
**import** android.os.Bundle;
**import** android.view.View;
**import** android.widget.AdapterView;
**import** android.widget.AdapterView.OnItemClickListener;
**import** android.widget.ArrayAdapter;
**import** android.widget.ListView;
**import** android.widget.TextView;
**import** android.widget.Toast;

**public class** OAuthPicasaActivity **extends** ListActivity {
  OAuthPicasaActivity act;

  /** Called when the activity is first created. */
  @Override
  **public void** onCreate(Bundle savedInstanceState) {
         **super**.onCreate(savedInstanceState);
         act = **this;**
         OAuth o = **new** OAuth**(this)**;
         Token t = o.getToken();

  if (!t.isValidForReq()) {
          Intent intent = **new** Intent(**this**, AuthActivity.**class**);
          **this**.startActivity(intent);
         }
         **if** (t.isExpired()) {
                 o.getRequestToken();
         }

         DataFetcher df = **new** DataFetcher(t);
         df.fetchAlbums("sheranapress");
         String[] names = **new** String[] {}; // Add bridge code here to parse XML
                                           // from DataFetcher and populate
                                           // your List

         **this**.setListAdapter(**new** ArrayAdapter  < String > (**this**, R.layout.*list_item*,
                           names));

         ListView lv = getListView();
         lv.setTextFilterEnabled(**true**);

         lv.setOnItemClickListener(**new** OnItemClickListener() {
                 **public void** onItemClick(AdapterView  <?>  parent, View view,
                                 **int** position, **long** id) {
                            Toast.*makeText* (getApplicationContext(),
                                              ((TextView) view).getText(),
 Toast. *LENGTH_SHORT*).show();
            }
       });

  }

}

您将看到这个文件正在做几件事情。首先,它实例化了 OAuth 类。接下来,它检索令牌对象,并测试该令牌是否有效,以便在 isValidForReq() 函数中发出请求。它还在 isExpired() 函数中测试令牌是否过期。如果令牌有效,则实例化 DataFetcher 对象,该对象向 Picasa 查询属于用户 sheranapress 的所有相册列表。这是在 df . fetchalbums(" sherana press ")行中完成的。

显然,这个应用第一次运行时,不会有有效的令牌对象。应用处理这种情况的方法是,首先获取一个授权代码,然后获取一个带有该授权代码的请求令牌(按照 Google 的 OAuth 2 规范)。接下来看看这是怎么做的。

处理授权

清单 8-2 显示了我们的应用中处理授权部分的源代码。如果您查看 doAuth() 函数,您将看到一个对 Google 的请求,应用在一个 WebView 对象中显示响应。WebView 对象是一个显示 HTML 内容的字段。你可以把它想象成一个简约的浏览器。这允许最终用户登录到她的谷歌帐户,并授予或拒绝我们的应用访问。用户将看到 Google 登录网页,并被要求使用她的凭证登录。这些凭证没有存储在我们应用的任何地方。如果他允许我们的应用使用她的 Picasa 流,那么 Google 会发回一个授权码。我们的应用将把这个授权码存储在令牌对象中。这是在 ClientHandler 对象中完成的(参见清单 8-3 )。

***清单 8-2。***Auth 活动获取授权码。

**package** net.zenconsult.android;

**import** java.net.URI;
**import** java.net.URISyntaxException;

**import** org.apache.http.message.BasicNameValuePair;

**import** android.app.Activity;
**import** android.content.Context;
**import** android.os.Bundle;
**import** android.util.Log;
**import** android.webkit.WebView;

**public class** AuthActivity **extends** Activity {
        **private** BasicNameValuePair clientId = new BasicNameValuePair("client_id",
                         "200744748489.apps.googleusercontent.com");
        **private** BasicNameValuePair clientSecret = new BasicNameValuePair(
                         "client_secret", "edxCTl_L8_SFl1rz2klZ4DbB");
        **private** BasicNameValuePair redirectURI = new BasicNameValuePair(
                         "redirect_uri", "urn:ietf:wg:oauth:2.0:oob");
        **private** String scope = "scope=[`picasaweb.google.com/data/`](https://picasaweb.google.com/data/)";
        **private** String oAuth = "[`accounts.google.com/o/oauth2/auth`](https://accounts.google.com/o/oauth2/auth) ?";
        **private** String httpReqPost = " [`accounts.google.com/o/oauth2/token`](https://accounts.google.com/o/oauth2/token) ";
        **private final** String FILENAME = ".oauth_settings";
        **private** URI uri;
        **private** WebView wv;
        **private** Context ctx;
        **private** Token token;

        @Override
        **public void** onCreate(Bundle savedInstanceState) {
               **super**.onCreate(savedInstanceState);
               setContentView(R.layout.auth);
               doAuth();
  }
  **public void** doAuth() {
          **try** {
                 uri = **new** URI(oAuth + clientId + "&" + redirectURI + "&" + scope
                              + "&response_type = code");
                 wv = (WebView) findViewById(R.id.webview);
                 wv.setWebChromeClient(**new** ClientHandler(**this**));
                 wv.setWebViewClient(**new** MWebClient());
                 wv.getSettings().setJavaScriptEnabled(**true**);
                 wv.loadUrl(uri.toASCIIString());
                 Log.v("OAUTH", "Calling " + uri.toASCIIString());
          }**catch** (URISyntaxException e) {
                 e.printStackTrace();
          }
    }
}

***清单 8-3。***client handler 将授权码写入令牌对象。

package net.zenconsult.android;

import android.app.Activity;
import android.util.Log;

import android.webkit.WebChromeClient;
import android.webkit.WebView;
import android.widget.Toast;

public class ClientHandler extends WebChromeClient {
       private Activity activity;
       private OAuth oAuth;

       public ClientHandler(Activity act) {
               activity = act;
               oAuth = new OAuth(activity);
       }

       @Override
        public void onReceivedTitle(WebView view, String title) {
               String code = "";
               if (title.contains("Success")) {
                      code = title.substring(title.indexOf(' = ') + 1, title.length());
                      setAuthCode(code);
                      Log.v("OAUTH", "Code is " + code);
                      oAuth.getRequestToken();
                      oAuth.writeToken(oAuth.getToken());
                      Toast toast = Toast.makeText(activity.getApplicationContext(),
                                      "Authorization Successful", Toast.LENGTH_SHORT);
                      toast.show();
                      activity.finish();
               } else if (title.contains("Denied")) {
                      code = title.substring(title.indexOf(' = ') + 1, title.length());
                      setAuthCode(code);
                      Log.v("OAUTH", "Denied, error was " + code);
                      Toast toast = Toast.makeText(activity.getApplicationContext(),
                                      "Authorization Failed", Toast.LENGTH_SHORT);
                      toast.show();
                      activity.finish();
               }
         }

  public String getAuthCode() {
          return oAuth.getToken().getAuthCode();
  }

  public void setAuthCode(String authCode) {
          oAuth.getToken().setAuthCode(authCode);
  }

  @Override
  public void onProgressChanged(WebView view, int progress) {

  }
}

把 ClientHandler 想象成一个观察者。它在每个 HTML 网页中寻找一个特定的字符串——“成功”——。如果它找到了这个词,那么我们就获得了正确的授权码,这意味着我们的最终用户已经批准了我们的访问。

将授权码写入内部存储后,您将需要获取一个请求令牌。在 Oauth 中,您将需要一个请求令牌来开始请求访问任何资源的过程。OAuth 流程请参见图 6-25。如果您再次查看我们的 ClientHandler 代码,您将会看到下面几行代码 oAuth.getRequestToken() 和 oauth . write token(oauth . gettoken())。这两行使用实例化的 OAuth 类(参见清单 8-4 )请求一个请求令牌,然后将其写入内存。 getRequestToken() 函数处理该部分。同样值得注意的是,每当我提到存储时,您都应该考虑使用加密。有关实施安全数据存储的更多信息,请参考第五章中的“Android 中的数据存储”部分。

清单 8-4。 如果授权码有效,OAuth 类从 Google 获取请求令牌。

**package** net.zenconsult.android;

**import** java.io.BufferedInputStream;
**import** java.io.BufferedOutputStream;
**import** java.io.File;
**import** java.io.FileInputStream;
**import** java.io.FileNotFoundException;
**import** java.io.FileOutputStream;
**import** java.io.IOException;
**import** java.io.ObjectInputStream;
**import** java.io.ObjectOutputStream;
**import** java.io.StreamCorruptedException;
**import** java.io.UnsupportedEncodingException;
**import** java.net.URI;
**import** java.util.ArrayList;
**import** java.util.List;

**import** org.apache.http.HttpEntity;
**import** org.apache.http.HttpResponse;
**import** org.apache.http.NameValuePair;
**import** org.apache.http.client.ClientProtocolException;
**import** org.apache.http.client.HttpClient;
**import** org.apache.http.client.entity.UrlEncodedFormEntity;
**import** org.apache.http.client.methods.HttpPost;
**import** org.apache.http.impl.client.DefaultHttpClient;
**import** org.apache.http.message.BasicNameValuePair;
**import** org.apache.http.util.EntityUtils;
**import** org.json.JSONException;
**import** org.json.JSONObject;

**import** android.app.Activity;
**import** android.content.Context;
**import** android.util.Log;
**import** android.webkit.WebView;
**import** android.widget.Toast;

**public class** OAuth {
        **private** BasicNameValuePair clientId = **new** BasicNameValuePair("client_id",
                        "200744748489.apps.googleusercontent.com");
        **private** BasicNameValuePair clientSecret = **new** BasicNameValuePair(
                        "client_secret", "edxCTl_L8_SFl1rz2klZ4DbB");
        **private** BasicNameValuePair redirectURI = **new** BasicNameValuePair(
                        "redirect_uri", "urn:ietf:wg:oauth:2.0:oob");
        **private** String scope = "scope=[`picasaweb.google.com/data/`](https://picasaweb.google.com/data/)";
        **private** String oAuth = "[`accounts.google.com/o/oauth2/auth`](https://accounts.google.com/o/oauth2/auth)?";
        **private** String httpReqPost = "[`accounts.google.com/o/oauth2/token`](https://accounts.google.com/o/oauth2/token)";
        **private** final String FILENAME = ".oauth_settings";
        **private** URI uri;
        **private** WebView wv;
        **private** Context ctx;
        **private** Activity activity;
        **private boolean** authenticated;
        **private** Token token;

        **public** OAuth(Activity act) {
                ctx = act.getApplicationContext();
                activity = act;
                token = readToken();

        }

        **public** Token readToken() {
                Token token = **null**;
                FileInputStream fis;
                **try** {
                        fis = ctx.openFileInput(FILENAME);
                        ObjectInputStream in = **new** ObjectInputStream(
                                       **new** BufferedInputStream(fis));
                        token = (Token) in.readObject();
                        **if** (token == **null**) {
                                token = **new** Token();
                                writeToken(token);
                        } 
                        in.close();
                        fis.close();
                } **catch** (FileNotFoundException e) {
                        writeToken(**new** Token());
                } **catch** (StreamCorruptedException e) {
                        // **TODO** Auto-generated catch block
                        e.printStackTrace();
                } **catch** (IOException e) {
                        // **TODO** Auto-generated catch block
                        e.printStackTrace();
                } **catch** (ClassNotFoundException e) {
                        // **TODO** Auto-generated catch block
                        e.printStackTrace();
                }
                **return** token;
                }

        **public void** writeToken(Token token) {
                **try** {
                        File f = **new** File(FILENAME);
                        **if** (f.exists()) {
                                f.delete();
                        }
                        FileOutputStream fos = ctx.openFileOutput(FILENAME,
                                       Context.*MODE_PRIVATE*);

                        ObjectOutputStream out = **new** ObjectOutputStream(
                                       **new** BufferedOutputStream(fos));
                        out.writeObject(token);
                        out.close();
                        fos.close();
                } **catch** (FileNotFoundException e1) {
                        Log.*e*("OAUTH", "Error creating settings file");
                } **catch** (IOException e2) {
                        // **TODO** Auto-generated catch block
                        e2.printStackTrace();
                }
        }

        **public void** getRequestToken() {
                HttpClient httpClient = **new** DefaultHttpClient();
                HttpPost post = **new** HttpPost(httpReqPost);
                List  <  NameValuePair  > nvPairs = **new** ArrayList  <  NameValuePair  >  ();
                nvPairs.add(clientId);
                nvPairs.add(clientSecret);
                nvPairs.add(**new** BasicNameValuePair("code", token.getAuthCode()));
                nvPairs.add(redirectURI);
                nvPairs.add(**new** BasicNameValuePair("grant_type", "authorization_code"));
                **try** {
                        post.setEntity(**new** UrlEncodedFormEntity(nvPairs));
                        HttpResponse response = httpClient.execute(post);
                        HttpEntity httpEntity = response.getEntity();
                        String line = EntityUtils.*toString*(httpEntity);
                        JSONObject jObj = **new** JSONObject(line);
                        token.buildToken(jObj);
                        writeToken(token);
                } **catch** (UnsupportedEncodingException e) {
                        // **TODO** Auto-generated catch block
                        e.printStackTrace();
                } **catch** (ClientProtocolException e) {
                        // **TODO** Auto-generated catch block
                        e.printStackTrace();
                } **catch** (IOException e) {
                        **if** (e.getMessage().equals("No peer certificate")) {
                                Toast toast = Toast.*makeText*![image](https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/bc8661c9c71b4058b9114e0c0cd140db~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1775475979&x-signature=9FpKZBqkeeYzS5BvQavRLU3waTQ%3D)
(activity.getApplicationContext(),
                                                "Possible HTC Error for Android 2.3.3",
                                                Toast.*LENGTH_SHORT*);
                            toast.show();
                        }
                        Log.*e*("OAUTH", "IOException " + e.getMessage());
                } **catch** (JSONException e) {
                        // **TODO** Auto-generated catch block
                        e.printStackTrace();
                }

        }

        **public** Token getToken() {
                **return** token;
        }

        **public void** setToken(Token token) {
                **this**.token = token;
        }
}

您可能已经注意到,该令牌被用作单例令牌。它被写入设备的内部存储器并从中读取。这允许应用的不同区域在身份验证过程的不同阶段对其进行读写。理想情况下,这应该是同步的,以确保读和写只发生在一个类中。

我已经为清单 8-5 中的令牌对象提供了源代码。该对象实现了可序列化的接口;因此,它可以完整地写入内部存储。确保通过数据存储加密器运行它,以增加安全性。除了检查自己的到期日期之外,令牌对象几乎不包含任何逻辑。

清单 8-5。 令牌对象

package net.zenconsult.android;

**import** java.io.Serializable;
**import** java.util.Calendar;
**import** org.json.JSONException;
**import** org.json.JSONObject;
**public class** Token **implements** Serializable {
        /**
        *
        */
        **private static final long** serialVersionUID = 6534067628631656760L;
        **private** String refreshToken;
        **private**  String accessToken;
        **private**  Calendar expiryDate;
        **private**  String authCode;
        **private**  String tokenType;
        **private**  String name;

        **public** Token() {
                setExpiryDate(0);
                setTokenType("");
                setAccessToken("");
                setRefreshToken("");
                setName("");
        }

        **public** Token(JSONObject response) {
                try {
                      setExpiryDate(response.getInt("expires_in"));
                } **catch** (JSONException e) {
                      setExpiryDate(0);
                }
                try {
                      setTokenType(response.getString("token_type"));
                } **catch** (JSONException e) {
                      setTokenType("");
                }
                try {
                      setAccessToken(response.getString("access_token"));
                } **catch** (JSONException e) {
                      setAccessToken("");
                }
                try {
                      setRefreshToken(response.getString("refresh_token"));
                } **catch** (JSONException e) {
                      setRefreshToken("");
                }
        }
        **public void** buildToken(JSONObject response) {
                try {
                      setExpiryDate(response.getInt("expires_in"));
                } catch (JSONException e) {
                      setExpiryDate(0);
                }
                try {
                      setTokenType(response.getString("token_type"));
                } catch (JSONException e) {
                      setTokenType("");
                }
                try {
                      setAccessToken(response.getString("access_token"));
                } catch (JSONException e) {
                      setAccessToken("");
                }
                try {
                      setRefreshToken(response.getString("refresh_token"));
                } catch (JSONException e) {
                      setRefreshToken("");
                }
        }

        **public boolean** isValidForReq() {
                if (getAccessToken() != null && !getAccessToken().equals("")) {
                      return true;
                } else {
                      return false;
                }
        }

        **public boolean**  isExpired() {
                Calendar now = Calendar.getInstance();
                if (now.after(getExpiryDate()))
                      return true;
                else
                      return false;
        }

        **public** String getRefreshToken() {
                      return refreshToken;
        }

        **public void** setRefreshToken(String refreshToken) {
                if (refreshToken == null)
                        refreshToken = "";
                        this.refreshToken = refreshToken;
        }

        **public** String getAccessToken() {
                      return accessToken;
        }

        **public void** setAccessToken(String accessToken) {
                if (accessToken == null)
                        accessToken = "";
                this.accessToken = accessToken;
        }

        **public** Calendar getExpiryDate() {
                      return expiryDate;
        }

        **public void** setExpiryDate(int seconds) {
                Calendar now = Calendar.getInstance();
                now.add(Calendar.SECOND, seconds);
                this.expiryDate = now;
        }

        **public** String getAuthCode() {
                      return authCode;
        }

        **public void** setAuthCode(String authCode) {
                if (authCode == null)
                authCode = "";
                this.authCode = authCode;
        }

        **public** String getTokenType() {
                      return tokenType;
        }

        **public void** setTokenType(String tokenType) {
                if (tokenType == null)
                tokenType = "";
                this.tokenType = tokenType;
        }

        **public** String getName() {
                      return name;
        }

        **public void** setName(String name) {
                this.name = name;
        }
}

最后,还有数据提取器类(见清单 8-6 )。您使用该类对 Picasa 进行所有受保护的查询。例如,您可以使用这个类来获取相册和照片,甚至上传照片。Picasa 以 XML 格式发回所有回复(注意,我省略了 XML 解析组件)。如果你想知道如何编写一个简单的 XML 解析器来读取 Picasa 的响应,那么看看这本书的附录。

***清单 8-6。***data etcher 类

**package** net.zenconsult.android;

**import** java.io.IOException;

**import** org.apache.http.HttpEntity;
**import** org.apache.http.HttpResponse;
**import** org.apache.http.client.ClientProtocolException;
**import** org.apache.http.client.HttpClient;
**import** org.apache.http.client.methods.HttpGet;
**import** org.apache.http.impl.client.DefaultHttpClient;
**import** org.apache.http.util.EntityUtils;

**public** class DataFetcher {
        **private** HttpClient httpClient;

        **private** Token token;
        **public** DataFetcher(Token t) {
                token = t;
                httpClient = **new** DefaultHttpClient();
        }
        **public void** fetchAlbums(String userId) {
                String url = " [`picasaweb.google.com/data/feed/api/user/`](https://picasaweb.google.com/data/feed/api/user/) "
                                  + userId;
                **try** {
                     HttpResponse resp = httpClient.execute(buildGet(
                                   token.getAccessToken(), url));
                     **if** (resp.getStatusLine().getStatusCode() == 200) {
                                          HttpEntity httpEntity = resp.getEntity();
                                          String line = EntityUtils. *toString*(httpEntity);
                                          // Do your XML Parsing here
                                          }
                } **catch** (ClientProtocolException e) {
                                          // TODO Auto-generated **catch** block
                                          e.printStackTrace();
                } **catch** (IOException e) {
                                          // TODO Auto-generated **catch** block
                                          e.printStackTrace();
        }
        }
        **public** HttpGet buildGet(String accessToken, String url) {
                HttpGet get = **new** HttpGet(url);
                get.addHeader("Authorization", "Bearer " + accessToken);
                return get;
        }
}

挑战响应

我们在第六章的中非常简要地讨论了基于挑战响应的认证。让我们仔细看看挑战-响应认证技术。以下是所需步骤 ?? 的简要概述,也显示在图 8-2 中。请记住,这只是服务器对客户端进行身份验证的单向身份验证:1。

  1. 客户端请求安全资源。
  2. 服务器发送一个质询字符串 c。
  3. 客户端生成一个随机字符串 r。
  4. 客户端根据 C、R 和用户密码生成一个散列。
  5. 客户端将 R 和散列发送回服务器。
  6. 服务器根据存储的用户密码和。
  7. 如果验证正确,服务器发回请求的资源;否则,会发回一条错误消息。

9781430240624_Fig08-02.jpg

图 8-2 。质询-响应会话期间客户端和服务器之间数据交换的图形表示

注意您还可以有一个相互认证的场景,其中客户端对服务器进行认证。

让我们编写一些简单的代码,帮助我们在应用中使用挑战-响应认证技术。您应该发展这些代码段以适应您自己的需要,然后在您的应用中使用它们。它们有助于减少终端用户的暴露,因为您不会在设备上存储任何凭据。我已经给出了客户端和服务器端代码的例子。服务器端的代码是用 Java 写的,可以打包成 Java Web 存档文件(WAR 文件)。要测试它,将它打包成一个 WAR 文件,并简单地将它放到 servlet 容器或应用服务器的部署目录中。

让我们从服务器端代码开始。我们将创建一个 Java servlet 来处理与客户端的 HTTP 通信。图 8-3 显示了项目结构。这个结构说明了我们有一个只有四个文件的相当简单的项目。

9781430240624_Fig08-03.jpg

图 8-3 。我们的挑战响应服务器端项目结构

其中之一,Hex.java 文件 ,是我用来将各种数据类型转换成十六进制字符串的工具类;另一个,【Constants.java】,保存用户名和密码。这些凭证将用于比较客户端输入的内容。

您还会注意到,我们使用 Apache Commons 编解码器库来帮助我们的 Base64 编码和解码。在本例中,我们采用 CRAM-MD5 身份验证方法,改为使用 SHA1 哈希。(CRAM 是挑战响应认证机制。)

我将首先展示代码,然后解释我们要做什么。让我们从我们的 servletLogin.java 开始,如清单 8-7 所示。该代码有两个主要分支:

  • 主分支 1 处理接收到没有“挑战”参数的请求的情况。
  • 主分支 2 处理接收到带有“挑战”参数的请求的情况。

清单 8-7。 登录类

**package** net.zenconsult.android;

**import** java.io.IOException;

**import** javax.servlet.ServletException;
**import** javax.servlet.annotation.WebServlet;
**import** javax.servlet.http.HttpServlet;
**import** javax.servlet.http.HttpServletRequest;
**import** javax.servlet.http.HttpServletResponse;
**import** javax.servlet.http.HttpSession;

/**
        * Servlet implementation class login
        */
@WebServlet(description = "Login Servlet", urlPatterns = { "/login" })
        **public class** Login **extends** HttpServlet {
                **private static final long** serialVersionUID = 1 L;

        /**
                * **@see** HttpServlet#HttpServlet()
                */
                **public** Login() {
                **super()**;
                **// TODO** Auto-generated constructor stub
                }

                /**
                * **@see** HttpServlet#doGet(HttpServletRequest request, HttpServletResponse
                * response)
                */
        **protected void** doGet(HttpServletRequest request,
                        HttpServletResponse response) **throws** ServletException,![image](https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/bc8661c9c71b4058b9114e0c0cd140db~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1775475979&x-signature=9FpKZBqkeeYzS5BvQavRLU3waTQ%3D)
  IOException {
                HttpSession session = request.getSession();
                String param = request.getParameter("challenge");
                **if** (param != **null**) {
                CRAM c = (CRAM) session.getAttribute("challenge");
                **if** (c == **null**) {
                c = **new** CRAM();
                session.setAttribute("challenge", c);
                response.setHeader("Content-Type", "text/xml");
                response.getWriter().write(c.generate());
                } **else** {
                **if** (c.verifyChallenge(param.trim())) {
                response.setHeader("Content-Type", "text/xml");
                response.getWriter().write![image](https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/bc8661c9c71b4058b9114e0c0cd140db~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1775475979&x-signature=9FpKZBqkeeYzS5BvQavRLU3waTQ%3D)
(c.generateReply("Authorized"));
                session.invalidate();
                } **else** {
                response.setHeader("Content-Type", "text/xml");
                response.getWriter().write![image](https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/bc8661c9c71b4058b9114e0c0cd140db~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1775475979&x-signature=9FpKZBqkeeYzS5BvQavRLU3waTQ%3D)
(c.generateReply("Unauthorized"));
                session.invalidate();
                }
                }
                } **else** {
                CRAM c = **new** CRAM();
                session.setAttribute("challenge", c);
                response.setHeader("Content-Type", "text/xml");
                response.getWriter().write(c.generate());
                }

                }

                /**
                * **@see** HttpServlet#doPost(HttpServletRequest request, HttpServletResponse
                * response)
                */
                **protected void** doPost(HttpServletRequest request,
                HttpServletResponse response) **throws** ServletException,![image](https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/bc8661c9c71b4058b9114e0c0cd140db~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1775475979&x-signature=9FpKZBqkeeYzS5BvQavRLU3waTQ%3D)
                IOException {
                **// TODO** Auto-generated method stub
                }
}

在每种情况下,我们都在创建一个 CRAM 对象。该对象将生成我们的质询字符串,并对用户响应进行比较。我们将 CRAM 对象与每个 HTTP 会话相关联,以便使用相同的挑战字节进行验证。

现在将是一个很好的时间来从协议层面看一下客户端和服务器之间发生了什么(见图 8-4 )。整个流程有四个步骤,非常简单:

  1. 客户端请求受保护的资源。
  2. 服务器回复一个询问。
  3. 客户端使用最终用户凭证来计算响应,并将其发送回服务器。
  4. 最后,服务器将计算相同的响应,进行比较,并决定用户是否被授权。

9781430240624_Fig08-04.jpg

图 8-4 。挑战响应消息流

所有这些都是在不通过 Web 发送用户凭证的情况下完成的。

CRAM 对象 的源代码如清单 8-8 所示。

清单 8-8。 补习班

**package** net.zenconsult.android;

**import** java.io.StringWriter;
**import** java.security.InvalidKeyException;
**import** java.security.NoSuchAlgorithmException;
**import** java.security.SecureRandom;
**import** javax.crypto.Mac;

**import** javax.crypto.SecretKey;
**import** javax.crypto.spec.SecretKeySpec;
**import** javax.xml.parsers.DocumentBuilder;
**import** javax.xml.parsers.DocumentBuilderFactory;
**import** javax.xml.parsers.ParserConfigurationException;
**import** javax.xml.transform.OutputKeys;
**import** javax.xml.transform.Transformer;
**import** javax.xml.transform.TransformerConfigurationException;
**import** javax.xml.transform.TransformerException;
**import** javax.xml.transform.TransformerFactory;
**import** javax.xml.transform.dom.DOMSource;
**import** javax.xml.transform.stream.StreamResult;

**import** org.apache.commons.codec.binary.Base64;
**import** org.w3c.dom.Document;
**import** org.w3c.dom.Element;
**import** org.w3c.dom.Text;

**public class** CRAM **implements** Constants {
  **private final byte[]** secret = **new byte[32];**

         **public** CRAM() {
                 SecureRandom sr = **new** SecureRandom();
                 sr.nextBytes(secret);
                 }

         **public** String generate() {
                 DocumentBuilderFactory dbFactory = DocumentBuilderFactory.*newInstance*();
                 DocumentBuilder dBuilder = **null**;
                 try {
                       dBuilder = dbFactory.newDocumentBuilder();
                 } **catch** (ParserConfigurationException e) {
                       // **TODO** Auto-generated catch block
                       e.printStackTrace();
                 }
                 Document doc = dBuilder.newDocument();

                 // Build Root
                 Element root = doc.createElement("ServerResponse");
                 doc.appendChild(root);

                 // Challenge Section
                 Element authChallenge = doc.createElement("AuthChallenge");
                 root.appendChild(authChallenge);

                 // The Challenge
                 Element challenge = doc.createElement("Challenge");
                 Text challengeText = doc.createTextNode(Base64
                               .*encodeBase64String*(secret));
                 challenge.appendChild(challengeText);
                 authChallenge.appendChild(challenge);

                 TransformerFactory tFactory = TransformerFactory.*newInstance*();
                 Transformer transformer = **null**;
                 **try** {
                       transformer = tFactory.newTransformer();
                 } **catch** (TransformerConfigurationException e) {
                       // **TODO** Auto-generated catch block
                       e.printStackTrace();
                 }
                 transformer.setOutputProperty(OutputKeys.*OMIT_XML_DECLARATION*, "yes");
                 transformer.setOutputProperty(OutputKeys.*INDENT*, "yes");
                 StringWriter sw = **new** StringWriter();
                 StreamResult res = **new** StreamResult(sw);
                 DOMSource source = **new** DOMSource(doc);
                 **try** {
                       transformer.transform(source, res);
                 } **catch** (TransformerException e) {
                       // **TODO** Auto-generated catch block
                       e.printStackTrace();
                 }
                 String xml = sw.toString();
                 **return** xml;
                 }
         **public boolean** verifyChallenge(String userResponse) {
                 String algo = "HmacSHA1";
                 Mac mac = **null**;
                 **try** {
                        mac = Mac.*getInstance*(algo);
                 } **catch** (NoSuchAlgorithmException e) {
                       // **TODO** Auto-generated catch block
                       e.printStackTrace();
                 }
                 SecretKey key = **new** SecretKeySpec(*PASSWORD*.getBytes(), algo);

                 **try** {
                 mac.init(key);
                 } **catch** (InvalidKeyException e) {
                       // **TODO** Auto-generated catch block
                       e.printStackTrace();
                 }
                 String tmpHash = *USERNAME* + " " + Hex.*toHex*(mac.doFinal(secret));
                 String hash = Base64.*encodeBase64String*(tmpHash.getBytes());
                 **return** hash.equals(userResponse);
                 }

                 public String generateReply(String response) {
                 DocumentBuilderFactory dbFactory = DocumentBuilderFactory.*newInstance*();
                 DocumentBuilder dBuilder = **null**;
                 **try** {
                 dBuilder = dbFactory.newDocumentBuilder();
                 } **catch** (ParserConfigurationException e) {
                       // **TODO** Auto-generated catch block
                       e.printStackTrace();
                 }
                 Document doc = dBuilder.newDocument();

                 // Build Root
                 Element root = doc.createElement("ServerResponse");
                 doc.appendChild(root);

                 // Challenge Section
                 Element authChallenge = doc.createElement("AuthChallenge");
                 root.appendChild(authChallenge);

                 // Reply
                 Element challenge = doc.createElement("Response");
                 Text challengeText = doc.createTextNode(response);
                 challenge.appendChild(challengeText);
                 authChallenge.appendChild(challenge);

                 TransformerFactory tFactory = TransformerFactory.*newInstance*();
                 Transformer transformer = **null**;
                 **try** {
                 transformer = tFactory.newTransformer();
                 } **catch** (TransformerConfigurationException e) {
                       // **TODO** Auto-generated catch block
                       e.printStackTrace();
                 }
                 transformer.setOutputProperty(OutputKeys.*OMIT_XML_DECLARATION*, "yes");
                 transformer.setOutputProperty(OutputKeys.*INDENT*, "yes");
                 StringWriter sw = **new** StringWriter();
                 StreamResult res = **new** StreamResult(sw);
                 DOMSource source = **new** DOMSource(doc);
                 **try** {
                 transformer.transform(source, res);
                 } **catch** (TransformerException e) {
                       // **TODO** Auto-generated catch block
                       e.printStackTrace();
                 }
                 String xml = sw.toString();
                 **return** xml;
         }

}

在实例化 CRAM 对象时,生成一个新的 32 字节随机数。这是一个领域,而且它与补习对象密切相关。这个随机字节串将用于进一步的质询生成和响应验证。

接下来是 generate() 函数 ,它只不过是为我们生成的随机字节创建一个 Base64 编码。然后,它创建一个 XML 响应和这个质询字符串,然后将它返回给 servlet,以便发送给最终用户。

下一个函数 verify challenge(String user response)是一个重要的函数。如果使用了正确的凭据,它将生成客户端应该生成的响应。使用存储的用户密码,通过 HMAC-SHA1 算法对原始随机字节序列进行哈希运算。然后用户名被添加到这个散列的前面,并进行 Base64 编码。接下来,将它与客户端响应进行比较,当然,如果用户名和密码输入正确,客户端响应应该是相同的 — 。

最后,generate reply(String response)函数将把响应变量中指定的单词作为 XML 文本发回。servlet 根据客户端响应 是否正确,使用以下任意一个词调用该函数:

  • 【授权】
  • “未授权”

您还可以设置一个特殊的授权 cookie 来表明会话已经过身份验证。有许多方法可以改进和构建这些代码。我在这里包含了基本代码,这样您可以更好地理解如何在您的前端和后端应用中实现挑战-响应身份验证机制。

现在我们已经看了服务器端代码 ,让我们为客户端写一些代码。我已经在图 8-5 中展示了项目结构。同样,skeletal 项目相当简单,只有三个文件,不包括十六进制函数类。我将带你浏览每个文件的功能,从入口点开始,【ChallengeResponseClientActivity.java】的(见清单 8-9 )。创建一个 Comms 对象(参见清单 8-10 )和一个 CRAM 对象(参见清单 8-11 )的代码相当简单。 Comms 对象处理客户端和服务器之间的所有网络通信,而 CRAM 对象处理哈希生成部分。在服务器端, CRAM 对象与 CRAM 对象非常相似。在这种情况下,没有验证组件,因为客户端不验证服务器。相反, CRAM 对象使用 HMAC-SHA1 来计算基于服务器挑战的散列。

清单 8-9。 切入点和主要活动

**package** net.zenconsult.android;

**import** android.app.Activity;
**import** android.os.Bundle;
**import** android.view.View;
**import** android.widget.Button;
**import** android.widget.Toast;

**public class** ChallengeResponseClientActivity **extends** Activity {
         /** Called when the activity is first created. */
         @Override
         **public void** onCreate(Bundle savedInstanceState) {
                 **super**.onCreate(savedInstanceState);
                 setContentView(R.layout. *main*); 
                 **final** Activity activity = **this;**

                 **final** Button button = (Button) findViewById(R.id.*button1*);
                 button.setOnClickListener(new View.OnClickListener() {
                         **public void** onClick(View v) {
                                 Comms c = new Comms(activity);
                                        String challenge = c.getChallenge();
                                        CRAM cram = new CRAM(activity);
                                        String hash = cram.generate(challenge);
                                        String reply = c.sendResponse(hash);
                                        **if** (c.authorized(reply)) {
                                                 Toast toast = Toast.*makeText*(
                                                         activity![image](https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/bc8661c9c71b4058b9114e0c0cd140db~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1775475979&x-signature=9FpKZBqkeeYzS5BvQavRLU3waTQ%3D)
.getApplicationContext(), "Login success",
         Toast.*LENGTH_LONG*);
                                            toast.show();
                                        } **else** {
                                                 Toast toast = Toast.*makeText*(
                                                         activity![image](https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/bc8661c9c71b4058b9114e0c0cd140db~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1775475979&x-signature=9FpKZBqkeeYzS5BvQavRLU3waTQ%3D)
.getApplicationContext(), "Login failed",
         Toast.*LENGTH_LONG*);
                                                 toast.show();
                                        }
                             }
                 });
         }
}

***清单 8-10。***Comms 类处理这个应用的所有 HTTP 请求。

package net.zenconsult.android;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.utils.URLEncodedUtils;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import org.w3c.dom.Document;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import android.app.Activity;
import android.content.Context;
import android.util.Log;
import android.widget.Toast;

public class Comms {
  private final String url = "[`192.168.3.117:8080/ChallengeResponse/login`](http://192.168.3.117:8080/ChallengeResponse/login)";
  private Context ctx;
  private DefaultHttpClient client;

  public Comms(Activity act) {
  ctx = act.getApplicationContext();
  client = new DefaultHttpClient();
  }

  public String sendResponse(String hash) {
  List  <  NameValuePair  >  params = new ArrayList  <  NameValuePair  >  ();
  params.add(new BasicNameValuePair("challenge", hash));
  String paramString = URLEncodedUtils.*format*(params, "utf-8");
  String cUrl = url + "?" + paramString;
  return doGetAsString(cUrl);
  }

  public boolean authorized(String response) {
  InputStream is = new ByteArrayInputStream(response.getBytes());
  DocumentBuilderFactory dbFactory = DocumentBuilderFactory.*newInstance*();
  DocumentBuilder db = null;
  Document doc = null;
  String reply = "";
  try {
  db = dbFactory.newDocumentBuilder();
  doc = db.parse(is);
  NodeList nl = doc.getElementsByTagName("Response");
  reply = nl.item(0).getTextContent();
  is.close();
  } catch (ParserConfigurationException e) {
  // TODO Auto-generated catch block
  e.printStackTrace();
  } catch (SAXException e) {
  // TODO Auto-generated catch block
  e.printStackTrace();
  } catch (IOException e) {
  // TODO Auto-generated catch block
  e.printStackTrace();
  }
  return reply.matches("Authorized");
  }

  public String getChallenge() {
  InputStream challengeText = doGetAsInputStream(url);
  DocumentBuilderFactory dbFactory = DocumentBuilderFactory.*newInstance*();
  DocumentBuilder db = null;
  Document doc = null;
  String challenge = "";
  try {
  db = dbFactory.newDocumentBuilder();
  doc = db.parse(challengeText);
  NodeList nl = doc.getElementsByTagName("Challenge");
  challenge = nl.item(0).getTextContent();
  challengeText.close();
  } catch (SAXException e) {
  // TODO Auto-generated catch block
  e.printStackTrace();
  } catch (IOException e) {
  // TODO Auto-generated catch block
  e.printStackTrace();
  } catch (ParserConfigurationException e) {
  // TODO Auto-generated catch block
  e.printStackTrace();
  }
  return challenge;
  }

  public String doGetAsString(String url) {
  HttpGet request = new HttpGet(url);
  String result = "";
  try {
  HttpResponse response = client.execute(request);
  int code = response.getStatusLine().getStatusCode();
  if (code == 200) {
  result = EntityUtils.*toString*(response.getEntity());
  } else {
  Toast toast = Toast.*makeText*(ctx, "Status Code " + code,
  Toast.*LENGTH_SHORT*);
  toast.show();
  }
  } catch (ClientProtocolException e) {
  // TODO Auto-generated catch block
  e.printStackTrace();
  } catch (IOException e) {
  // TODO Auto-generated catch block
  e.printStackTrace();
  }
  return result;

  }

  public InputStream doGetAsInputStream(String url) {
  HttpGet request = new HttpGet(url);
  InputStream result = null;
  try {
  HttpResponse response = client.execute(request);
  int code = response.getStatusLine().getStatusCode();
  if (code == 200) {
  result = response.getEntity().getContent();
  } else {
  Toast toast = Toast.*makeText*(ctx, "Status Code " + code,
  Toast.*LENGTH_SHORT*);
  toast.show();
  }
  } catch (ClientProtocolException e) {
  // TODO Auto-generated catch block
  e.printStackTrace();
  } catch (IOException e) {
  // TODO Auto-generated catch block
  e.printStackTrace();
  }
  return result;

  }
}

清单 8-11。 补习班

**package** net.zenconsult.android;

**import** java.security.InvalidKeyException;
**import** java.security.NoSuchAlgorithmException;

**import** javax.crypto.Mac;
**import** javax.crypto.SecretKey;
**import** javax.crypto.spec.SecretKeySpec;

**import** android.app.Activity;
**import** android.util.Base64;
**import** android.widget.TextView;

 **public class** CRAM {
        **private Activity** activity;

        **public** CRAM(Activity act) {
                 activity = act;
  }

        **public** String generate(String serverChallenge) {
                String algo = "HmacSHA1";
                TextView pass = (TextView) activity.findViewById(R.id.*editText2*);
                **byte[]** server = Base64.*decode*(serverChallenge, Base64.*DEFAULT*);

                Mac mac = null;
                **try** {
                        mac = Mac.*getInstance*(algo);
                } **catch** (NoSuchAlgorithmException e) {
                        // **TODO** Auto-generated catch block
                        e.printStackTrace();
                }
                String keyText = pass.getText().toString();
                SecretKey key = **new** SecretKeySpec(keyText.getBytes(), algo);
                **try** {
                        mac.init(key);
                } **catch** (InvalidKeyException e) {
                        // **TODO** Auto-generated catch block
                        e.printStackTrace();
                }
                **byte[]** tmpHash = mac.doFinal(server);
                TextView user = (TextView) activity.findViewById(R.id.*editText1*);
                String username = user.getText().toString();
                String concat = username + " " + Hex.*toHex*(tmpHash);
                String hash = Base64.*encodeToString*(concat.getBytes(), Base64.*URL_SAFE*);
                **return** hash;
        }
}

在客户端 ,如果一切按计划进行,你的 app 会弹出一条精彩的“登录成功”的消息来迎接你,如图图 8-5 所示。

9781430240624_Fig08-05.jpg

图 8-5 成功的挑战-响应身份验证

摘要

我希望这些例子能让您更好地理解如何在移动和后端 web 应用中实现替代的身份验证机制。通过减少对用户凭据存储的依赖,您可以显著提高应用的安全性。

在前端和后端代码中实现 OAuth 不会是最容易完成的事情。然而,花费一些初始努力并为将来的代码准备一组可重用的库是值得的。补习也是一样。由于所涉及的工作量,这些身份验证方法并不是许多开发人员首先要考虑的。但是,它可以确保您的应用比那些通过网络存储和转发用户凭据的应用更安全。

希望你会认为你到目前为止学到的东西是有用的。我的希望是,你会相信你没有浪费时间在这个新协议的缩写上,它被称为挑战响应认证协议。