跨站请求

到目前为止,您已经修改了 StockWatcher 应用的初始实现,该应用在客户端代码中模拟了股票数据。当前的实现现在从本地服务器检索 JSON 格式的数据。

在本节中,您将调用远程服务器。为此,您需要绕过 SOP(同源策略)约束。

  1. 查看要求和设计:访问限制和异步通信。
  2. 在远程服务器上创建一个 JSON 数据源。
  3. 从远程服务器请求数据。
  4. 处理响应。
  5. 测试。

注意:有关 GWT 应用中客户端-服务器通信的更广泛指南,请参阅 与服务器通信

开始之前

StockWatcher 项目

本教程基于在 构建示例 GWT 应用 教程中创建的 GWT 概念和 StockWatcher 应用。如果您尚未完成构建示例 GWT 应用教程,并且熟悉基本的 GWT 概念,则可以导入到目前为止编码的 StockWatcher 项目。

  1. 下载 StockWatcher 项目(包含 JSON)。
  2. 解压缩文件。
  3. 将项目导入 Eclipse

    1. 文件菜单中,选择导入...菜单选项。
    2. 选择导入源“常规”>“将现有项目导入工作区”。单击“下一步”按钮。
    3. 对于根目录,浏览并选择 StockWatcher 目录(来自解压缩的文件)。单击“完成”按钮。

如果您使用的是 ant,请编辑 StockWatcher/build.xml 中的gwt.sdk属性,使其指向您解压缩 GWT 的位置。

为了实际运行本教程,您将需要访问除 StockWatcher 运行所在服务器之外的另一个服务器(能够运行 PHP 脚本),或者能够在您的机器上运行 Python 脚本。

查看要求和设计

当您修改 StockWatcher 的当前实现以访问远程服务器上的数据时,需要解决两个问题

  • 访问限制:SOP(同源策略)
  • 异步通信

访问限制:同源策略

同源策略 (SOP) 是一种浏览器安全措施,它限制客户端 JavaScript 代码与来自同一域名、协议和端口的资源进行交互。浏览器仅在以下三个值相同的情况下才认为两个页面具有相同的来源。例如,如果 StockWatcher 应用在 http://abc.com:80 上的网页中运行,它无法与从其他域名 http://xyz.com 加载的股票数据进行交互。即使端口不同,它也无法从同一域名加载股票数据,例如 http://abc.com:81

SOP 背后的理念是,出于安全原因,浏览器不应信任从任意网站加载的内容。恶意网页可能会注入窃取数据或以其他方式损害安全的代码。

因此,对于从另一个域名或端口访问 JSON 格式的股票数据,当前的实现将无法正常工作。Web 浏览器将阻止检索 JSON 的 HTTP 调用。

有关 SOP 及其对 GWT 的影响的更详细描述,请阅读 什么是同源策略,它如何影响 GWT?

绕过 SOP

有两种方法可以绕过 SOP 安全性

  • 您自己的服务器上的代理
  • 将 JSON 响应加载到<script>标记中
您自己的服务器上的代理

第一个选项是遵循 SOP 规则,并在您的本地服务器上创建一个代理。然后,您可以向您的本地服务器发出 HTTP 调用,并让它从远程服务器获取数据。这样做有效,因为在您的 Web 服务器上运行的代码不受 SOP 限制。只有客户端代码受限。

具体而言,对于 StockWatcher 应用,您可以通过编写服务器端代码来从远程服务器下载(并可能缓存)JSON 编码的股票报价来实现此策略。然后,您可以使用我们想要的任何机制从本地服务器检索数据:GWT RPC 或使用 RequestBuilder 进行直接 HTTP。

这种方法的一个缺点是它需要额外的服务器端代码。另一个缺点是,额外的 HTTP 调用会增加远程调用的延迟,并增加 Web 服务器的工作量。

将 JSON 响应加载到<script>标记中

另一个选项是动态地将 JavaScript 加载到<script>标记中。客户端 JavaScript 可以像在 HTML 文档对象模型 (DOM) 中的其他任何元素一样操作<script>标记。客户端代码可以将<script>标记的 src 属性设置为自动下载和执行新 JavaScript 到页面中。

此策略不受 SOP 限制。因此,您可以有效地使用它从远程服务器加载 JavaScript(因此也加载 JSON)。

您将使用此策略从远程服务器获取 JSON 格式的股票数据。

异步通信

动态地将 JavaScript 加载到<script>标记中解决了 SOP 问题,但引入了另一个问题。当您使用此方法加载 JavaScript 时,虽然浏览器会异步检索代码,但它不会在完成时通知您。相反,它只是执行新的 JavaScript。但是,根据定义,JSON 不能包含可执行代码。将这两者放在一起,您会意识到您无法使用<script>标记加载纯 JSON 数据。

带填充的 JSON (JSONP)

为了解决回调问题,您可以将回调函数的名称指定为调用本身的输入参数。然后,Web 服务器将在对该函数的调用中包装 JSON 响应。此技术称为带填充的 JSON (JSONP)。当浏览器完成下载<script>标记的新内容时,回调函数会执行。

callback125([{"symbol":"DDD","price":10.610339195026,"change":0.053085447454327}]);

Google 数据 API 支持此技术。

对于 StockWatcher,客户端代码中的额外要求是,您需要在 HTTP 请求中包含要使用的 JavaScript 函数的名称。

实现策略

现在您已经了解了围绕跨站请求的 SOP 问题,请将此实现与从本地服务器获取 JSON 数据的实现进行比较。您需要更改一些现有的实现,但您也可以重用一些组件。大部分工作将在编写新方法 getJSON 中进行,该方法会向远程服务器发出调用。

任务 同站实现 跨站实现
发出调用 使用 Request Builder 的 HTTP 使用 Jsonp Request Builder 的 JSON-P。
服务器端代码 返回 JSON 字符串 返回带有 JSON 字符串的 JavaScript 回调函数
处理响应 使用JsonUtils.safeEval()将 JSON 字符串转换为 JavaScript 对象 已经是 JavaScript 对象;将其转换为 StockData 数组
数据对象 创建一个覆盖类型:StockData 重用覆盖类型
处理错误 创建一个 Label 小部件来显示错误消息 重用 Label 小部件

创建数据源

在本教程中,您有两个选项来设置股票数据,以便 StockWatcher 遇到 SOP 限制。

  1. 如果您有权访问安装了 PHP 的服务器,则可以使用以下 PHP 脚本生成 JSON 格式的股票数据。
  2. 如果您没有服务器,但机器上安装了 Python,则可以使用以下 Python 脚本从与 StockWatcher 运行的端口不同的端口提供股票数据。

实际使用不同的服务器

如果您有权访问 Web 服务器,则可以使用以下 PHP 脚本返回 JSONP。

  1. 创建一个文本文件,并将其命名为stockPrices.php *
<?php

  header('Content-Type: text/javascript');
  header('Cache-Control: no-cache');
  header('Pragma: no-cache');

  define("MAX_PRICE", 100.0); // $100.00
  define("MAX_PRICE_CHANGE", 0.02); // +/- 2%

  $callback = trim($_GET['callback']);
  echo $callback;
  echo '([';

  $q = trim($_GET['q']);
  if ($q) {
    $symbols = explode(' ', $q);

    for ($i=0; $i<count($symbols); $i++) {
      $price = lcg_value() * MAX_PRICE;
      $change = $price * MAX_PRICE_CHANGE * (lcg_value() * 2.0 - 1.0);

      echo '{';
      echo "\"symbol\":\"$symbols[$i]\",";
      echo "\"price\":$price,";
      echo "\"change\":$change";
      echo '}';

      if ($i < (count($symbols) - 1)) {
        echo ',';
      }
    }
  }

  echo ']);';
?>
  1. 将 PHP 脚本复制到另一个服务器。
  2. 打开浏览器并请求 JSON 数据。
    • http://_[www.myStockServerDomain.com]_/stockPrices.php?q=ABC
  3. 返回 JSON 字符串。
    • [{"symbol":"ABC","price":81.284083,"change":-0.007986}]
    • 但是,正如您将在下一节中看到的那样,StockWatcher 应用程序将无法从其客户端代码发出此请求。
  4. 通过附加回调函数的名称来请求 JSONP。
    • http://_[www.myStockServerDomain.com]_/stockPrices.php?q=ABC&callback=callback125
  5. JSON 被嵌入回调函数中返回。
    • callback125([{"symbol":"ABC","price":53.554212,"change":0.584011}]);

模拟第二台服务器

如果您无法访问远程服务器,但您的本地机器上安装了 Python,则可以模拟远程服务器。如果您向不同的端口发出 HTTP 请求,您将遇到与尝试访问不同域时相同的 SOP 限制。

使用以下脚本从本地机器上的不同端口提供数据。对于每个股票代码,Python 脚本都会以 JSON 格式生成随机价格和变动值。请注意,在 BaseHTTPServer.HTTPServer 构造函数中,它将在端口 8000 上运行。另请注意,该脚本支持回调查询字符串参数。

  1. 创建一个 Python 脚本并将其保存为 quoteServer.py *
#!/usr/bin/env python2.4
#
# Copyright 2007 Google Inc. All Rights Reserved.

import BaseHTTPServer
import SimpleHTTPServer
import urllib
import random

MAX_PRICE = 100.0
MAX_PRICE_CHANGE = 0.02

class MyHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):

  def do_GET(self):
    form = {}
    if self.path.find('?') > -1:
      queryStr = self.path.split('?')[1]
      form = dict([queryParam.split('=') for queryParam in queryStr.split('&amp;')])

      body = '['

      if 'q' in form:
        quotes = []

        for symbol in urllib.unquote_plus(form['q']).split(' '):
          price = random.random() * MAX_PRICE
          change = price * MAX_PRICE_CHANGE * (random.random() * 2.0 - 1.0)
          quotes.append(('{"symbol":"%s","price":%f,"change":%f}'
                       % (symbol, price, change)))

        body += ','.join(quotes)

      body += ']'

      if 'callback' in form:
        body = ('%s(%s);' % (form['callback'], body))

    self.send_response(200)
    self.send_header('Content-Type', 'text/javascript')
    self.send_header('Content-Length', len(body))
    self.send_header('Expires', '-1')
    self.send_header('Cache-Control', 'no-cache')
    self.send_header('Pragma', 'no-cache')
    self.end_headers()

    self.wfile.write(body)
    self.wfile.flush()
    self.connection.shutdown(1)

bhs = BaseHTTPServer.HTTPServer(('', 8000), MyHandler)
bhs.serve_forever()
  1. 将脚本保存到主 StockWatcher 目录。*
  2. 确保 Python 解释器位于您的 PATH 中。
  3. 启动脚本。
    • 从命令行输入 python quoteServer.py
    • 服务器将启动,但您不会立即看到任何输出。(它将记录每个 HTTP 请求)。
  4. 打开浏览器并请求 JSON 数据。
    • https://127.0.0.1:8000/?q=ABC
  5. 返回 JSON 字符串。
    • [{"symbol":"ABC","price":81.284083,"change":-0.007986}]
    • 但是,正如您将在下一节中看到的那样,StockWatcher 应用程序将无法从其客户端代码发出此请求。
  6. 通过附加回调函数的名称来请求 JSONP。
    • https://127.0.0.1:8000/?q=ABC&callback=callback125
  7. JSON 被嵌入回调函数中返回。
    • callback125([{"symbol":"ABC","price":53.554212,"change":0.584011}]);

从远程服务器请求数据

现在您已经验证服务器是否以 JSON 字符串或 JSONP 的形式返回股票数据,您可以更新 StockWatcher 来请求并处理 JSONP。

RequestBuilder 代码被对 JsonpRequestBuilder 的调用替换。

  1. 在 StockWatcher 类中,更改 JSON_URL 常量,如下所示

    * * 更改

private static final String JSON_URL = GWT.getModuleBaseURL() + "stockPrices?q=";
  • 如果您的股票数据是从不同的端口(Python 脚本)提供的,请将 JSON_URL 更改为
private static final String JSON_URL = "https://127.0.0.1:8000/?q=";
  • 如果您的股票数据是从不同的域(PHP 脚本)提供的,请指定域和 stockPrices.php 脚本的完整路径
private static final String JSON_URL = "http://_www.myStockServerDomain.com_/stockPrices.php?q=";
  1. 尝试从远程服务器检索纯 JSON 数据。
    • 在开发模式下调试 StockWatcher。
    • 输入股票代码。
  2. StockWatcher 显示错误消息:无法检索 JSON。
    • 要修复 SOP 错误,在下一步中,您将使用 JsonpRequestBuilder。

更新 refreshWatchList 方法

1. * 更新 refreshWatchList 方法。 *

  /**
   * Generate random stock prices.
   */
  private void refreshWatchList() {
    if (stocks.size() == 0) {
      return;
    }

    String url = JSON_URL;

    // Append watch list stock symbols to query URL.
    Iterator<String> iter = stocks.iterator();
    while (iter.hasNext()) {
      url += iter.next();
      if (iter.hasNext()) {
        url += "+";
      }
    }

    url = URL.encode(url);

    JsonpRequestBuilder builder = new JsonpRequestBuilder();
    builder.requestObject(url, new AsyncCallback<JsArray<StockData>>() {
      public void onFailure(Throwable caught) {
        displayError("Couldn't retrieve JSON");
      }
      public void onSuccess(JsArray<StockData> data) {
        // TODO handle JSON response
      }
    });
  }
  1. 如果您还没有删除 RequestBuilder 代码,请将其删除。
    • RequestBuilder 代码被对 JsonpRequestBuilder 的调用替换。因此,您不再需要在 refreshWatchList 方法中使用以下代码
    // Send request to server and catch any errors.
    RequestBuilder builder = new RequestBuilder(RequestBuilder.GET, url);

    try {
      Request request = builder.sendRequest(null, new RequestCallback() {
        public void onError(Request request, Throwable exception) {
          displayError("Couldn't retrieve JSON");
        }

        public void onResponseReceived(Request request, Response response) {
          if (200 == response.getStatusCode()) {
            updateTable(JsonUtils.safeEval(response.getText()));
          } else {
            displayError("Couldn't retrieve JSON (" + response.getStatusText()
                + ")");
          }
        }
      });
    } catch (RequestException e) {
      displayError("Couldn't retrieve JSON");
    }

实现 onSuccess 方法

如果您从服务器收到响应,则调用 updateTable 方法来填充价格和变动字段。您仍然可以使用在同一站点实现中编写的覆盖类型(StockData)和 JsArray。

如果服务器没有返回响应,则显示一条消息。您可以使用在同一站点实现中创建的相同 displayError 方法和 Label 小部件。

  1. 对于 onSuccess 方法,将 TODO 注释替换为以下代码。
if (data == null) {
  displayError("Couldn't retrieve JSON");
  return;
}

updateTable(data);

测试

无论您选择从不同的域还是不同的端口提供 JSON 格式的股票数据,新的 StockWatcher 实现都应该绕过任何 SOP 访问限制,并能够检索股票数据。

在开发模式下测试

从不同的端口提供股票数据

  1. 确保 Python 服务器正在运行。
    • 如果它没有运行,请在命令行中输入 python quoteServer.py
  2. 在以开发模式运行的浏览器中,刷新 StockWatcher。
  3. 添加股票代码。
    • StockWatcher 显示价格和变动数据。这些信息现在来自不同的端口。
  4. 关闭 Python 服务器。
    • StockWatcher 显示错误:无法检索 JSON
  5. 重新启动 Python 服务器。
    • StockWatcher 清除错误并继续显示价格和变动更新。

从不同的域提供股票数据

  1. 在以开发模式运行的浏览器中,刷新 StockWatcher。
  2. 添加股票代码。
    • StockWatcher 显示价格和变动数据。这些信息现在来自远程服务器。
  3. 在 StockWatcher.java 中,更改 JSON_URL 以使其不正确。
  4. 在以开发模式运行的浏览器中,刷新 StockWatcher。
    • 添加股票代码。
    • StockWatcher 显示错误:无法检索 JSON
  5. 在 StockWatcher.java 中,更正 JSON_URL。
  6. 在以开发模式运行的浏览器中,刷新 StockWatcher。
    • 添加股票代码。
    • StockWatcher 清除错误并继续显示价格和变动更新。

下一步

安全和跨站点请求

在您实现自己的混合应用程序之前,请记住,下载跨站点 JSON 很强大,但也可能存在安全风险。确保您交互的服务器 **绝对值得信赖**,因为它们将能够在您的应用程序中执行任意 JavaScript 代码。花点时间阅读 GWT 应用程序的安全,其中描述了对 GWT 应用程序的潜在威胁以及如何防范这些威胁。