第 4 部分:REST 和 Ajax 应用程序的安全性及性能调优


注意: 本文在编写和测试时使用的是 NetBeans IDE 6.8

简介

本文是由四部分组成的系列文章的最后一部分,前三篇文章如下:

在本文中,我们将探讨一些高级话题,例如安全性和性能。我们将介绍如何保护应用程序免受 XSS(跨站点脚本)攻击,以及如何提高服务器端和客户端应用程序的安全性和性能。

本文使用本系列前几篇文章中开发的 ArticleEvaluator 示例应用程序。您可以使用第一篇文章结尾提供的链接下载第一篇文章的示例应用程序。同样,您也可以使用第二和第三篇文章结尾提供的链接下载相应的示例应用程序。

与本系列的前三篇文章相同,可以使用 NetBeans 6.8 或 6.9 开发示例应用程序。

安全性


实现安全性的传统方法:保护 URL 和对象

通常,从两个级别保障 Java 应用程序的安全:URL 级和对象级。

开发人员通常使用标准的 Java Platform Enterprise Edition (Java EE) 安全性来保护 URL,它允许您在 web.xml 文件中添加 URL 约束。另一种类似的非常流行的方法是使用 servlet 筛选器。举例来说,Spring Security(一种流行的开源安全框架)通常用于实现此目的。

对于我们的示例应用程序而言,应用程序的 JavaServer Faces 部分用于管理文章、作者和投票,因此也应对其提供保护。一种可满足此安全要求的非常常用的解决方案是添加一个规则,指明 faces/* URL 只能由具备管理员角色的用户使用。

此外,还可以在类级或方法级为对象提供保护。在这种方法中,批注或面向方面编程 (AOP) 用于控制哪些人可以执行哪些操作。这通常是在应用程序的业务层实现,因此表示层只能执行定义明确、受到保护的业务方法。

现在,我们测试这种方法,即使用 Enterprise JavaBeans (EJB) fr.responcia.otn.articleevaluator.AuthorFacade 对象来保护 create 方法:

@RolesAllowed("Administrator")
public void create(Author author) {
    em.persist(author);
}


如果您现在尝试使用 JavaServer Faces 应用程序创建新作者,则会遇到以下安全错误:

INFO: JACC Policy Provider: Failed Permission Check, context(ArticleEvaluator/ArticleEvaluator_internal)-
permission((javax.security.jacc.EJBMethodPermission AuthorFacade create,Local,fr.responcia.otn.articleevaluator.Author))
ATTENTION: A system exception occurred during an invocation on EJB AuthorFacade method public void
fr.responcia.otn.articleevaluator.AuthorFacade.create(fr.responcia.otn.articleevaluator.Author)
javax.ejb.AccessLocalException: Client not authorized for this invocation.
at com.sun.ejb.containers.BaseContainer.preInvoke(BaseContainer.java:1801)
at com.sun.ejb.containers.EJBLocalObjectInvocationHandler.invoke(EJBLocalObjectInvocationHandler.java:188)
at com.sun.ejb.containers.EJBLocalObjectInvocationHandlerDelegate.invoke(EJBLocalObjectInvocationHandlerDelegate.java:84)
at $Proxy233.create(Unknown Source)
at fr.responcia.otn.articleevaluator.__EJB31_Generated__AuthorFacade__Intf____Bean__.create(Unknown Source)
at fr.responcia.otn.articleevaluator.AuthorController.create(AuthorController.java:82)


我们可以看到,@RolesAllowed 批注阻止了不具备所需授权的用户对方法进行调用。

现在,我们已经测试了该安全措施,您可以从代码中删除该批注。

在 URL 级和对象级保护应用程序非常重要。这样做可以使攻击者很难访问或威胁受到保护的数据。在我们的示例中,如果同时应用了两种安全方法,攻击者将几乎无法添加新作者。

但是,我们会发现要防止黑客入侵我们的应用程序,仅靠这些措施是不够的。

攻击示例应用程序

我们将使用一个接近真实情况的案例。作为作者,我希望所有读者都给我的文章打五颗星。

身为作者,我对应用程序后端的访问会受到限制。我可以更新个人信息,如姓名、网站等。在上一节介绍的传统安全方法的保护下,这些信息可能是安全的。

我们先做一个简单的测试:使用 JavaServer Faces 应用程序编辑 1 号作者。

注意:如果您是第一次部署该应用程序(也就是说,您没有执行第一篇文章中的步骤),那么您的数据库将是空的。在这种情况下,您需要使用 JavaServer Faces 应用程序(部署在 http://localhost:8080/ArticleEvaluator 上)创建一个新作者(您不需要提供它的 ID;默认 ID 将为“1”)。您还需要一篇新文章,用于链接到该作者。

现在,如果您访问 REST 应用程序 (http://localhost:8080/ArticleEvaluator/article.html),那么应该可以在页面底部看到更新后的作者信息:

屏幕截图


我们来执行一次简单的入侵,返回 JavaServer Faces 前端并更改作者的姓氏,如下所示:

Julien <script type="text/javascript">alert("You have been hacked. Read this Security and Performance 
Tuning of a REST and Ajax Application article for details.");


如果我们返回 REST 前端,就会吃惊地看到以下页面:

屏幕截图


发生了什么?由于 NetBeans 生成的 JavaServer Faces 前端不知道我们可以在“first name”字段中插入 HTML,因此它不会检查我们输入的文本字符串的内容。这样一来,在 REST 前端阅读文章的普通用户就会执行作者恶意插入的 JavaScript 代码。当然,作者可以插入比警告框更有趣的东西,例如给自己投票打 5 颗星,然后隐藏投票面板!

这种攻击相对较为复杂,但在公共网站上已变得日益频繁。eBay 和 MySpace 就遭受了 XSS 等攻击,并且现在已经广为人知。如何才能防止这些些攻击呢?

最简单的方法是防止用户插入 HTML 和 JavaScript。这就是 Wiki 采用自己的语法的原因所在。事实上,许多网站仅仅是“避开”了用户输入的所有 HTML 字符。针对该问题的一种非常流行的解决方案是 Apache Commons 提供的 org.apache.commons.lang.StringEscapeUtils 类。

当然,这个解决方案对于较为复杂的网站来说是远远不够的。这就是为何市场上出现了各种专用于清理用户提供的 HTML 代码的解决方案。其中一些涉及使用黑名单(这就是 MySpace 采用的方式)或一些智能解析。AntiSamy 项目提供了这种解决方案的一个开源实现,允许您通过高级验证系统配置哪些用户可以或不可以输入内容。

性能


客户端性能为何如此重要

Yahoo! YSlow 等工具表明,与开发人员的普遍观点相反,应用程序的客户端是最容易发生性能问题的地方。下载和呈现一个复杂的 HTML 或 JavaScript 页面可能需要很长时间,特别是当用户使用旧版浏览器或者使用上网本等低性能计算机访问页面时。

因此,如果您希望获得流畅的用户体验,应将客户端代码与优化 Java 和 SQL 代码摆在同等重要位置。

如何提供静态数据

Ajax 应用程序通常会大量使用 JavaScript、图像和 CSS 文件。这正是我们的示例应用程序所发生的情况,它使用了 JQuery。

可以通过一些规则显著改善此类应用程序:

  • 所有静态数据都应单独提供。静态数据应由特定的 Web 服务器或内容传递网络 (CDN) 托管在一个所发送请求不包括 cookie 的域上。
  • 应对静态数据进行压缩。可以使用 gzip(现代浏览器均支持经过 gzip 压缩的数据)来压缩一些文件,但也可以运行一些针对 JavaScript 的特定压缩程序(例如 Dojo 的 ShrinkSafe)。
  • 应确保所提供的文件的数量最少,因为每个文件都需要一个单独的 HTTP 连接。虽然这对某些类型的文件来说非常简单(JavaScript 文件只需连接在一起即可),但对于图像来说则较为困难。对于图像而言,最好的方法就是 CSS (sprites),即使用 CSS 显示包含许多小图像的大图像的各个部分。
  • 应在提供这些文件的 HTTP 请求上配置“expires”、“last-modified” 和“ETag”头部。这样做可以使用户的浏览器将所有这些文件存储在缓存中,而不是每次访问时都要下载一次。这大大减少了下载文件的数量,但也可能会产生一个重要的问题:当您希望更改这些文件中的某个文件时,应该怎么做呢?如何强制用户的浏览器重新下载该文件呢?具体方法是在所有静态内容的 URL 中均使用版本号,然后每次在发布新版本时让该编号增加 1。

使用 gzip 和 HTTP 头部改善客户端性能

一些用于提供静态内容的方法同时也适用于动态内容。

最明显的解决方案是使用 gzip 压缩所有动态内容。如果您的网页足够大,那么这种方法可能会给客户端带来巨大的性能改善,因为网页的下载速度会变得更快。当然,客户端和服务器端都是要付出代价的,因为需要对内容进行压缩和解压缩。在服务器端,这种压缩和解压缩操作可由 Web 服务器(参见 Apache 的 mod_deflate 模块)、应用服务器或者仅由 servlet 筛选器来完成。

另一种更加高级的方法是采用与静态内容相同的方式,即使用 HTTP 头部。事实上,由于 REST 应用程序使用标准 URL,因此它的一些内容会被代理、前端 Web 服务器或客户端浏览器自动缓存。当然,这仅适用于信息变更不太频繁的情况。这对于 Wikipedia 来说是一种非常有效的方法,但对于 Twitter 来说则并非如此。

我们再来看看提供作者信息的 REST 服务。这些信息可能不会经常变更,并且也不需要实时更新,因此我们可以为它指定一些特定的 HTTP 头部以便对其进行缓存。

打开 AuthorResource 类(位于 fr.responcia.otn.articleevaluator.rest.resource 程序包下,或者您在创建本系列第一篇文章中的项目时所提供的包结构下),更改 get 方法:

@GET
@Produces({"application/xml", "application/json"})
public Response get(@QueryParam("expandLevel") 
        @DefaultValue("1")
        int expandLevel) {

    AuthorConverter converter = new AuthorConverter(getEntity(), 
                                    uriInfo.getAbsolutePath(), expandLevel);

    Calendar expiresDate = Calendar.getInstance();
    expiresDate.add(Calendar.DATE, 1);
    Response.ResponseBuilder response = Response.ok(converter).expires(expiresDate.getTime());
    return response.build();
}


此处,我们使用了 JSR 311 API(RESTful Web 服务)中一些更高级的方法,以便配置明天就要到期的资源。这是一种非常简单的要求客户端浏览器将作者信息缓存一天的方法。

可以通过以下方式测试该行为:

  1. 使用 REST 前端阅读一篇文章并留意作者的姓名。
  2. 使用 JavaServer Faces 前端更改作者姓名。

如果您返回 REST 前端,就会看到作者姓名没有发生更改。但是,如果您强制重新加载该页面,或者如果您清理浏览器缓存,作者的姓名会发生变化。

一种更加高级的系统将使用上次修改的头部或 ETag 头部。这种方法实现起来比较复杂,因为您需要知道资源何时发生了变化。对于简单的资源来说,这意味着您需要存储一个日期字段,并在每次更新时都更新该字段。对于对象图形等一些比较复杂的资源来说,您需要知道这些对象最后一次发生变更的时间,这显然非常难以实现。

因此,人们经常使用 servlet 筛选器来计算每个响应的消息摘要算法 5 (MD5) 散列值,然后使用该散列值作为 ETag 的关键字。Spring Framework 中的 org.springframework.web.filter.ShallowEtagHeaderFilter 类就是这种方法的一个绝佳示例。当然,您仍然需要先计算整个页面,然后才能计算 MD5 散列值。您会比以前消耗更多的 CPU 功率(因为您仍然需要生成页面,并且还需要计算 MD5 散列值),但您会获得更高的带宽,这意味着在客户端浏览器中加载页面的速度更快。

缓存内容以降低服务器端负载

在服务器端,我们的 REST 应用程序不同于任何经典的 Java EE 应用程序。我们应该适当地使用缓存来提高性能。由于我们使用的是标准 EJB 对象,因此可以使用最新的 Java Persistence API (JPA) 2.0 @Cacheable 批注。

打开 fr.responcia.otn.articleevaluator.Author 类,在其顶部添加该批注:

import javax.persistence.Cacheable;

@Entity
@Cacheable
public class Author implements Serializable {


添加该批注可以自动缓存 EJB 对象。测试该行为的一种简单方法是直接对数据库运行 SQL 查询,这会绕过缓存:

update AUTHOR set LASTNAME = "test"; 


如果您重新加载网页,作者的姓名应该保持不变,这表示缓存工作正常。

使用此方法可防止用户请求作者时数据库被击中,而这是 Java 应用程序遇到的头号性能问题。

总结

在本系列的四篇文章中,我们了解了如何构建一个基于 Jersey 的 RESTful 应用程序,以及如何通过 jQuery 框架对其实现高效访问。由于这两个框架都依赖于开放和标准的架构,因此可以很容易地让它们协同工作,即使对于 HTTP 缓存这样的高级任务也是如此。

我们还看到,这种应用程序可以提供一个高级用户界面(得益于 jQuery 的使用)并且易于伸缩(得益于采用了 REST 架构)。我们还研究了如何为这种应用程序提供保护,即使面对的是现代 XSS 攻击。

完成的示例应用程序可从此处下载:

JQuery 示例应用程序 (zip)

另请参阅

关于作者


Julien Dubois 是一位拥有 10 多年经验的 Java 开发人员和架构师。他与人合著了法国最畅销的有关 Spring Framework 的书籍,在 SpringSource 被 VMware 收购之前曾在该公司工作。目前,他领导和运营自己的咨询公司 Responcia,该公司提供法国知识管理解决方案 http://responcia.net


阅读本文的英文版本

前往 Java 中文社区论坛,发表您对本文的看法。