`
bigapple
  • 浏览: 1886 次
  • 性别: Icon_minigender_1
  • 来自: 大连
最近访客 更多访客>>
文章分类
社区版块
存档分类
最新评论

《Spring Web Flow权威指南》节选之Web开发的可控导航

阅读更多

1.1 自由浏览

以前, Web被设计为让用户可以浏览通过超链接连接在一起的信息页面的系统。信息通过HTML和内嵌的锚(anchor)来标注,通过HTML,信息能在浏览器中正确地显示出来,而通过锚,信息可以链接到其他资源或页面上。在Web中,每一个页面都是通过URI(Uniform Resource Identifier,通用资源标识符)来唯一识别的。

为了保持系统的扩展性和有效性,万维网被设计成了无状态的客户—服务器环境。客户浏览器通过HTTP协议中定义的GET方法从Web服务器上获取信息。这一简单的设置使得信息可以缓存在客户端和中间代理服务器上。而对于Web服务器自身,通过增加其他系统形成集群就能很容易地实现水平扩展,因为它无需维护与每一个浏览用户相关的信息。

无疑,这种简单的无状态架构是一个巨大的成功。万维网已经发展到了前所未有的程度,而数百万用户也已经熟悉了它的概念。浏览器提供导航帮助,如后退、前进、刷新按钮等,能让用户很方便地在不同页面间切换。用户也可以直接在浏览器地址栏中输入如http://en.wikipedia.org/wiki/Plato这样的URI地址,还可以把这些地址加入到收藏夹中供以后访问。浏览不受任何约束,用户浏览什么样的网页完全看他们的个人兴趣、时间和想象力等。

万维网的架构常被称为RESTful(具备表象化状态转移特征的),因为它遵循REST(Representational State Transfer,表象化状态转移)风格,REST最早由Roy Fielding在他的博士论文(2000)中作为分布式超媒体系统架构提出。

REST本意是唤起这样一种想象,即一个设计良好的Web应用程序是这样运行的:在网页的网络(虚拟状态机)中,用户在应用程序中选择一个链接(状态转换),导致下一个页面(代表程序的下一个状态)被传输并呈现给用户。

很多技术出版物中用“RESTful”描述使用简单Web技术(如HTML、HTTP以及URI)构建的应用程序。同时,有意义的、用户可读的URI通常被称为REST风格的URI或者URL。

当万维网的发展势头越来越强劲时,设计人员意识到如果能让用户参与交互,那么它将更加得力。为了满足这一需求,他们增加了一些交互性的元素以增强核心万维网标准,如下所述。

  • HTTP扩展了一个新的请求方法POST,通过它,可以把信息提交到服务器上。PUT和DELETE允许客户添加资源到服务器上,或者再把它们移除。
  • HTML中添加了允许用户交互的元素。最知名的例子是HTML的<FORM>标签,可以把键—值对的参数列表加到请求中,然后发送给服务器(通常是在POST请求中)。
  • 通过使用Cookie等技术,服务器可以在整个用户会话中,跨越多个请求跟踪用户的状态,因此,它可以在无状态的HTTP环境中工作,并能根据用户的不同而输出不同的内容。

所有这些技术和增强结合起来,产生了一个可以支持Web应用程序的系统,一个利用了万维网的广泛性和易用性的实用且具备交互能力的计算机应用程序。它带来了一些重要的优点,包括:

  • 客户无需安装自定义软件,这使得大规模的软件部署成为易事;
  • 所有客户使用的都是同一个、最新版本的应用系统;
  • 使得基于订阅的付费模型成为可能,从而有可能获得新的收入来源;
  • 用户已经熟悉了相关的技术——Web浏览器、HTML页面等,因此减少了培训的工作。

当然,新的机会难免要付出一些代价,如下所述。

  •  因为HTTP会话的存在,扩展Web应用程序比扩展传统的Web服务器更加困难。简单的解决方案又常常会导致服务器依赖现象(Server affinity),也就是要求活动会话中的某一用户总是使用集群中的某一个服务器,因而失去了失效转移和有效的负载均衡能力。不过,因为这个问题发生在服务器端,所以,可以由相关技术人员解决,而仍然保持客户浏览器的简单性和轻量性。
  • Web应用程序不能提供像桌面GUI应用程序那样丰富的交互式用户体验。这个缺点在一定程度上也是对用户有利的方面,因为它强制Web应用程序开发人员设计出简单且直观的用户界面来。

随着Web开发人员、用户以及业务人员在Web应用程序上积累的经验越来越多,对这些应用程序的需求也变得越来越高。今天,使用诸如AJAX(Asynchronous JavaScript and XML,异步JavaScript和XML)等技术开发的Web应用程序已经能提供更棒的用户体验。如今,Web应用程序在控制用户与应用程序交互时的动作方面也已经取得了长足的进步。下一节将深入讨论这个话题。

 

1.2 可控导航

大多数现代Web应用程序不仅要求自由浏览,而且有更苛刻的用例需要可控导航特性(Controued navigation),即限制用户导航的随意性,以正确地引导他们完成业务流程。关于可控导航,有很多广为人知的例子,如下所述。

  • 机票预订流程,其步骤包括航班选择、座位选择、飞机常客政策以及支付信息输入。同时,大多数旅行社不仅提供机票预订服务,还帮助你预订酒店和租车等。
  •  最近几年,网上银行也逐步为人们所广泛使用。顾客可以通过Web应用程序付款、购买股票或者管理信用卡等。
  •  许多政府机构使用Web技术和Web应用程序方便市民通过电子化方式来完成任务,如个税申报。这些流程通常和法律相关联,而且相当复杂,因此要求用户必须遵循正确的导航流程。

这些Web应用程序基本上用于处理敏感的用户数据甚至用户的金钱,因此,应用程序是否稳定、安全和健壮非常重要。好的应用程序应当让用户在使用的时候感觉到舒服,应当让用户相信它能做正确的事情,即使是不小心点了浏览器上的后退或者刷新按钮的情况下也能如此。

之前讨论过的所有Java Web应用程序框架能让你可以相对轻松地开发简单的Web应用程序。它们多数是使用HTTP会话来管理与特定用户相关联的状态的,同时使用自动数据绑定技术简化对包含HTML表单的页面的处理。然而,对于处理复杂的用例,如需要控制导航、应当引导用户完成某一任务等,它们的这点儿能力就显得力不从心了。

在Web应用程序中,正确无误地实现一个完全可控的导航是极难解决的问题,因此这种力不从心是不幸的。需要解决的三个主要问题是:

  •  导航的控制;
  •  状态管理;
  •  模块化。

接下来,详细研究一下这几个主题。

1.2.1 导航的控制

要实现Web应用程序中的可控导航,首先需要回答的问题是,“如何控制用户导航的随意性?”正如在1.1节所说明的,万维网被设计成一个可以自由浏览和无限制导航的系统。浏览器提供了收藏夹功能,用户可以将自己感兴趣的页面加入收藏夹,且在今后直接访问这些页面。浏览器保留的历史记录,以及后退、前进、刷新等按钮也为用户回到曾经浏览过的页面提供了方便。用户甚至可以同时打开多个浏览器窗口或者标签来做信息对比。

          也许你会想,这些功能只不过是现代浏览器提供给用户的便利罢了,但实际上,自由浏览的精神,已经根深蒂固地存在于为万维网提供动力的核心技术的规范中。以下HTTP 1.1规范的摘要(RFC 2616的13.13节)建议当用户使用后退按钮访问历史页面时,浏览器不应当从服务器上重新加载页面。

用户代理经常使用历史机制,如导航按钮和历史列表,重新显示会话里较早接收到的实体。

历史机制和缓存机制是不同的。尤其是,历史机制应该不会去尝试显示当前资源状态的语义透明视图。相反,它只是为了说明用户在获得资源时到底看到了什么。

默认情况下,过期时间不会应用于历史机制中。只要存储的实体仍然可用,即使它已经过期了,历史机制也应该显示它,除非用户配置了让用户代理刷新过期的文档。

该定义建议浏览器历史是作为客户端导航辅助用的,用于增强用户的网上冲浪体验。在创作本书的时候,多数流行的浏览器,如Mozilla的Firefox(http://www.mozilla.com/firefox)和微软的IE(http://www.microsoft.com/ie),都没有严格遵循本规范而是通过对缓存控制的设定来决定是否从历史当中重新加载页面。Opera浏览器(http://www.opera.com)是严格遵循本规范的浏览器典范。当单击后退按钮的时候,Opera会准确地显示前一个页面的内容,而不会从服务器重新加载。

          很显然,所有这些对自由浏览的支持并不能满足需要可控导航的用例。遗憾的是,通常,禁用浏览器的导航历史、收藏夹功能及新建窗口或者标签的功能是不可能的。因此,毫无疑问你需要处理眼下这些难题,即在具有可控对话的Web应用程序中,因用户使用自由浏览的便利性而产生的问题。一些需要处理的情形大致如下。

  •  如果用户把对话中间的某一页面加入收藏夹将会发生什么?我们无法阻止将页面加入到收藏夹中,但是用户使用收藏的页面回到对话中间时,应用程序该如何响应呢?多数情况下,答案是:应用程序生成一个错误,通知用户对话已经过期或者结束了,可能的话重新开始一个对话。对话或者任务的进入点也许是可收藏的,但是收藏中间的页面通常情况下是不可行的。在其他情况下,将保持跟踪对话一段时间,这样用户在稍后可以重新继续该对话。
  •  应用程序该如何处理刷新请求以及在浏览历史中后退或前进的请求?理想状况是,刷新请求是等幂的,反复使用不会产生其他的副作用,并且用户可以随意刷新页面。处理后退和前进则比较困难。
  •  为了比较结果或者权衡做出何种选择,用户通常会对同一应用程序打开多个浏览器窗口,当然这种情形不太普遍。那么,对于这种情形,Web应用程序又该如何处理呢?一定要谨慎对待这种情况,避免互相干扰或者重复提交。

应用程序可以忽略这些问题,而仅在启动需要可控导航的过程时,提示用户不要使用浏览器的后退按钮。显然,这种处理方法不仅很幼稚而且相当不友好,由于用户在因特网上冲浪时,已经习惯了频繁单击后退按钮或刷新按钮。当发生错误的时候,Web应用程序应当有能力以稳定而且可预知的方式来处理它。

对像内部网络中的应用程序而言,其用户群都是大家所熟悉的而且能有效地管理好,有时候可以避免这些问题。通过部署专有的或者自定义的Web浏览器,开发者可以完全禁用所有的导航辅助功能。而对于运行在因特网上的Web应用程序来说,这么做显然不可行,因为用户使用的浏览器各不相同。有些因特网Web应用程序通过在一个特殊的窗口中运行程序来模拟类似效果,这些特殊的窗口不包含按钮栏或者其他的装饰物。这能起到一定的作用,但是很容易被破坏,用户可以按键盘上的后退键或者鼠标的特定键后退到浏览历史的其他页面。

不完整的导航控制和用户无意中使用了浏览器的导航辅助功能还能导致Web应用程序中的另一个大问题:危险的二次提交。

1. 二次提交问题

你已经看到了用于万维网的HTTP协议所支持的两种主要的请求方法。请求方法用于表明向服务器发起的请求的意图是什么。

GET方法让客户可以从服务器获取资源(页面)。HTTP协议把GET方法定义为安全的方法,意思是说该方法应该只用于信息的获取而不会用于改变服务器的状态。换句话说,GET请求是幂等的,即使任意多次地重复刷新,结果也始终是相同的。GET是HTTP支持的第一个请求方法,当初也是唯一的一个。今天,它仍然是使用频率最高的一个请求方法。

POST方法则允许处理提交数据到服务的请求,因此,可能会带来一些副作用,如更新了数据库中的记录等。提交POST请求的用户应当自行负责由此造成的服务器状态的改变。POST请求是不安全的或者非幂等的,因此不能进行刷新或加入收藏。最初,POST请求是通过单击页面上的按钮发起的,而不是像GET请求那样通过文字链接的形式。按钮这种方式形象地将POST和GET请求加以区分,从而提示用户这两种方法的敏感性质。

通过浏览器的导航历史或者刷新按钮,用户可能会意外地重复某一POST请求,而导致服务器端二次处理问题的发生。这种重复处理有可能导致错误或者有害的负面影响,这就是所谓的二次提交(double submit)问题。设想一下,如果用户重复提交“确认购买”请求,结果会怎样?这必将导致两次购买!Web浏览器能察觉到这个问题的存在,当用户企图重复发起POST请求的时候,它会警告用户,在IE中会显示图1-2所示的警告。

 

相反,对于GET请求加载的页面是可以刷新的,也不会显示警告。虽然这些警告提供了一层安全保护,但它们会让最终用户感觉到唐突:只是不小心按了后退按钮,就导致了可能重复交易的的警告消息!

二次提交问题的真正的解决方案是POST-REDIRECT-GET模式(Jouravlev 2004)。Web应用程序响应POST请求时所发送的HTML页面对用户来说,与普通的网页没什么两样,它让用户觉得该页面是可收藏的,也是可以刷新的。在POST-REDIRECT-GET模式中,响应POST请求并不直接发送页面数据,而是首先发起一个REDIRECT请求,然后产生一个GET请求,该过程如图1-3所示。

 

图 1-3 POST-REDIRECT-GET模式

 

重定向请求指示Web浏览器使用GET请求到别处寻找响应结果。HTTP 1.1特为这种目的定义了一个结果代码303,“See Other”(摘自RFC2616的10.3.4节)。

请求的响应放在一个不同的URI下,应该用GET方法在那个资源上获得这一响应。此方法的存在主要是允许POST激活脚本的输出把用户代理重定向到选定的资源上。

为了向下兼容不支持HTTP 1.1的老版本浏览器,很多Web应用程序没有使用303响应,而用302,“Found”,代码(摘自RFC 2616的10.3.3节):。

请求的资源临时存在于不同的URI下。

如今,所有普遍使用的浏览器对302代码和303代码是一致对待的。

接到重定向响应的浏览器将会知道请求的资源可在另一个位置上找到。因此,它不会把原始的请求加入到浏览历史里,而仅仅添加重定向后的GET请求。该方法优雅地解决了二次提交的问题,因为重定向后的请求是可以安全地刷新的(它是GET请求)。更何况,导航的历史中只包含了等幂的GET请求,这种请求允许用户使用后退、前进按钮或者收藏页面,而不会出现警告消息,或者对服务器产生负面影响。

HTTP规范定义了GET和POST请求的语义。遗憾的是,无论是在HTTP协议中,还是Web服务器上这些语义都没有被强制要求。因此,很多网站和Web应用程序滥用GET请求,提交数据到服务器,带来了一些负面影响。这种折中措施通常是视觉上的一些需求或者是想把网页做得尽可能漂亮的愿望所驱使的。JavaScript给Web开发人员提供了很多自由空间,让他们能单击文字链接提交表单,或者单击按钮从服务器上获取资源。特别是图片按钮,它甚至给了你完全的视觉自由,却把GET和POST请求之间直观的区别给弄得模糊不清。

因为GET请求也可以用于提交数据到服务器上进行处理而可能导致副作用,所以POST-REDIRECT-GET模式应当用更加通用的方式来重新表述为“提交后重定向”,即Web应用程序应该针对所有的提交都发起重定向,而不论它是用POST还是用GET。

每次提交后都重定向的话,即使用户意外地使用了浏览器的后退按钮等功能,你所构建的Web应用程序也能够正确地运行。另一个有趣的问题是如何保护应用程序,防止恶意用户企图阻碍与Web应用程序对话。

2. 阻碍对话

Web应用程序中多数复杂的对话都要求直线行进,通常是一直进行到业务流程的结束,如填纳税表单等。用户输入所有必须的数据而不跳过重要的信息,以及不破坏敏感的校验等都是至关重要的。服务器必须仔细地跟踪用户的进展以确保能够按照业务需求定义的那样去完成任务。

客户仅提供它在对话中所处的当前位置信息是远远不够的。黑客通过操控请求的参数就能很容易地阻碍流的正常进行,甚至可能危害到整个应用程序及其管理的数据。

Web应用程序无法控制客户所使用的环境。普通的客户会使用各种流行的浏览器,而恶意用户则可能利用其他的工具来制造特殊的请求,欺骗服务器去做不应当做的事情。在Web应用程序开发人员中的一个规则是服务器必须始终验证用户输入的数据,即使已经有了客户端(JavaScript)的检验。对导航的控制来说,也是同样的道理。服务器有责任确保客户遵循定义好的导航规则。

总之,在Web应用程序中控制用户导航是一件非常棘手的事情。问题的根源在于一个不争的事实,那就是我们正尝试着在被明确设计为自由浏览的环境中强加导航限制。

 

1.2.2 状态管理

Web应用程序中复杂的对话不仅强制要求可控导航,而且也涉及状态管理问题。这些问题常常缠绕在一起:用户一边按照具体的业务流程所指定的导航流前进,一边收集状态信息。在使用浏览器的导航设施时,应用程序应当处理由此可能给对话相关的数据带来的影响,如下所述。

  •  当用户单击后退按钮时,应用程序是撤销(或忘记)后一页面上所做的修改呢,还是为了能让用户再次回到该页面时可以继续使用这些信息而保留所做的修改呢?这两种处理方式都比较常见。
  •  对于像Opera这样的浏览器,后退至历史浏览页面时,它根本不再和服务器通信。在这种情况下,该如何保持服务器和客户的状态一致呢?客户看到的有可能是与服务器上不同的陈旧数据,这可能会导致让人困惑的局面,甚至破坏数据。
  •  应用程序需要确保同一个对话的两个浏览器窗口之间不会互相干扰,譬如,不能用一个窗口中的数据覆盖了另一个窗口中的数据。
  •  当一个对话结束时,应当清空关联数据,这样做既能避免内存泄露,又能阻止同一任务被执行多次。

除了实际的应用程序数据之外,Web应用程序也需要维护那些跟踪用户在对话中进度的数据结构。如前所述,很有必要防止恶意用户阻碍对话。应用程序需要准确地知道用户在对话中所处的当前位置,及其在当前位置的导航选项。

多数Web应用程序框架都是跟踪HTTP 会话中与用户相关的状态。对于在整个用户对话中都必须可用的数据而言,如用户身份验证证书等,使用这种方法非常棒。它使得多个用户同时与Web应用程序交互时不会相互干扰。然而,HTTP 会话直接作为对话相关数据的存储介质就显得过于粗粒度了。例如,当两个浏览器窗口参与到同一个对话中时,如果对话的数据直接存储在HTTP会话中,通常就会相互干扰,因为这两个浏览器窗口是共享同一HTTP 会话的。

一个对话可以有多个浏览器窗口,而用户也可以打开多个浏览器窗口运行多个对话。在同一会话中同时执行多个对话这样的合理用例应当明确支持。

也需要清理与对话相关的所有数据。很显然,在对话结束时需要这么做,但是也需要让已经废弃的对话及时过期。在HTTP会话中存储对话关联的数据能起到一定作用,因为Servlet引擎在一段空闲时间后会自动清理过期的HTTP会话。

传统的用于控制接收到的请求及解决这些问题的设计模式是同步令牌,我们将在下面讨论。

同步令牌模式
同步令牌是一个知名的设计模式,可以帮助Web开发人员避免重复提交(Alur 2003)。譬如,在电子商务网站中,它可以用于防止用户重复提交同一订单。

同步令牌模式的工作方式是:当对话开始时,应用程序产生一个唯一的令牌并存储在HTTP会话中。该令牌将嵌入到对话过程中所生成的每一个页面里,而且对话的每一个请求里都应当包含该令牌的值。当请求到达服务器时,服务器比较HTTP会话中的令牌值和请求里面包含的令牌值。如果二者一致,则允许处理请求。如果不一致,就会产生一个错误,通知用户对话已经终止或者无效。对话结束时或者HTTP会话过期时,从HTTP会话中删除令牌。

传统意义上,同步令牌用于监控一个请求,而不是跨请求的对话。在这种情况下,针对每一个已经通过令牌验证的请求,会生成新的令牌值并存储在会话里,如图1-4所示。

图1-4 同步令牌模式

 

你可能会想这种模式和之前讨论过的POST-REDIRECT-GET模式是如何联系起来的呢。使用POST-REDIRECT-GET可以避免意外地二次提交同一个请求,但是不能阻止用户完成两次同一业务过程。这种业务过程通常由跨请求的多个页面组成的。同步令牌模式在POST-REDIRECT-GET模式上,增加附加的安全防护,防止有可能发生的有意地二次提交同一页面或任务。通常,这两种方法结合起来形成一个完整的解决方案。

一些Java Web应用程序框架,如Struts,内建对同步令牌模式的支持。不过,这种支持是较低层次的,需要开发人员手工在应用程序代码中加入令牌校验。

总之,万维网的无状态设计使得实现强大灵活且易于使用的状态管理系统并不那么轻而易举。框架应该解决这些共通问题,而不是交给应用程序开发人员来自行提供解决方案。

1.2.3 模块化问题

如果开发团队有能力增强基本的Web MVC框架来充分处理导航控制和状态管理问题,那么其所采用的解决方案极可能是粒度过于精细的。状态管理和导航控制代码通常遍及多个控制器或者组件,而不是被描述为良好定义的应用程序模块。

对话已经被表征为跨越多个请求并引导用户至任务或业务过程的终点。典型的业务过程具有与其他过程低耦合高内聚的特性,因而它可以作为模块化的理想候选。支撑框架应当能将对话建模为内聚的、自容纳的模块。这么做有如下几个好处。

  •  通过查看模块很容易就能找到业务过程的导航流,这些导航流通常定义在一个源代码文件中。这样,理解和可视化该过程就非常容易。在开发类似的模块时,可以直接利用业务专家以流程或者状态图的形式制作的输入数据,而无需担心被一大推技术细节困扰。
  •  粗粒度、定义良好且自容纳的模块,能够促进复用。如果实现了特定业务过程的模块定义了清晰的输入/输出契约,那么就可以像黑盒一样地复用它。一个模块可以使用其他模块作为其子模块,以实现常见的用法场景,如主从画面等。
0
0
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics