设计表现层时需要考虑的几个问题
开发者在设计表现层时,可以使用不同的模型,这时需要考虑一些相关的设计问题。这些问题和模型关系的紧密程度也各有不同,它们可以影响系统的各个方面,包括有安全、数据完整性、可管理性和扩展性。虽然这些设计问题大部分都可以用模型的形式表示,但我们不打算这样做,因为这样更为抽象,我们选择以非正式的文档形式表示。我们只是根据不同的模型,将每个需要考虑的问题列出来。
Session管理
用户Session指的是跨越一个客户和服务器多个请求间的一个对话。我们将在以下部分根据用户Session的概念讨论这个问题。
客户端的Session状态
在客户端保存Session的状态指的是将Session的状态串行化并且嵌入到返回给客户的HTML页面中。
在客户端保存Session的状态有这以下的好处:
. 它实现起来相对容易
. 在保存少量的状态信息时,它工作得很好
此外,这个策略还消除了跨越多个服务器复制状态的问题,例如多个服务器间实现负载均衡时就会遇到这种情况。
在客户端保存Session状态通常有两个方法--HTML的隐藏字段和HTTP cookies--我们将在下面讨论这些策略。第三个策略则是在每个页面的URL中嵌入Session状态信息,例如<form action=someServlet?var1=x&var2=y method=GET>。虽然第三个方法比较少见,但它也有着其它两个方法的许多限制。
HTML的隐藏字段(HTML Hidden Fields)
虽然这个方法实现起来相对容易,不过使用HTML隐藏字段在客户端保存Session状态仍然有着许多的缺点。这些缺点在保存大量的状态时尤为突出。保存大量的状态将会对性能有很大的影响。因为每次发出请求和响应时,都需要在网络中传送这些状态信息。
此外,当你利用隐藏的字段来保存Session状态时,这些持久的状态值只能是字符串值,因此所有的对象引用都必须被“字符串化”,而这些信息除非经过特别的加密,否则都是以明文的形式显示在HTML的源代码中。
HTTP Cookies
与隐藏字段的方法一样,使用HTTP Cookies的方式也是相对简单的。不幸的是,这两个方法有着许多相同的缺点。特别是,在保存大量的状态信息时将会对性能产生很大的影响,因为在每次的请求和响应时,都必须在网络上传送全部的Session状态信息。
在客户端保存Session状态时,我们也会遇到大小和类型的局限问题。cookie headers的大小是有限制的,这样就限制了可以被持久保存的数据量,而且和隐藏字段的方法一样,当你使用cookies来保存Session状态时,这些持久的状态信息只能使用字符串值。
在客户端保存Session状态会带来的安全问题
当你在客户端保存Session状态时,你必须考虑到由此带来的安全问题。如果你不想数据暴露给客户端,你就需要一些方法来加密数据,从而保证数据的安全。
虽然在客户端保存Session状态相对容易实现,不过它有着很多的缺点,这些都要我们花费时间去解决。对于需要处理大量数据的项目,特别是企业的系统,使用这种方式是得不偿失的。
表现层的Session状态
当Session状态保存在服务器端时,它使用一个Session ID得到,并且会一直保持住,直到发生以下的情形:
. 一个预定义的Session超时发生了
. Session被手动设置为无效
. 状态由Session中移除
要注意的是服务器关闭后,一些内存中的Session管理机制可能不能恢复。
很明显,对于要保存大量Session状态的应用,将它们的Session状态放在服务器是更好的。当状态被保存在服务器上时,你不会有客户端Session管理的大小和类型限制。此外,还避免了由此带来的安全问题,而且也不会遇到由于在每个请求间传送Session状态带来的性能影响。
使用该方式,你可以更加灵活地作处理,并且便于扩展和提高性能。
如果你在服务器上保存Session状态,你必须要决定如何使该状态信息被每个服务器得到,即你运行该应用的服务器。如果群集的软件是运行在负载均衡的硬件上,那么就要处理这个Session状态的复制问题,这是一个多维的问题,不过,众多的应用服务器现在都提供了各种各样的解决方案。也就是说,在应用服务器的级别上有解决的方法。其中的一个方法是保证用户只与一个服务器打交道,它在流量管理软件上用得比较多,例如Resonate [Resonate]的软件,在用户的Session中,该用户发出的每个请求都会被路由到同一个服务器处理。这种方式也被称为server affinity。
另一个可选的方式是在商业层或者资源层保存Session状态。企业JavaBeans组件可用来在商业层保存Session的状态,而一个关系数据库则可用在资源层。
控制客户访问
有很多时候我们都要限制或者控制客户端访问某些应用资源。下面我们就来讨论其中两种这样的情形。
限制或者控制客户访问的一个原因是防止一个视图或者部分的视图被一个客户直接访问。这个问题会发生在以下情况,例如仅有注册或者登陆后的用户才可允许访问一个特别的视图,或者是根据用户的角色限制用户访问部分的视图。
在描述过这个问题后,我们将讨论第二种情况,它和控制应用中一个用户的流程有关。后者的讨论和重复的form提交有关,因为多次提交将会导致不必要的重复事务。
控制视图访问
在一些情况下,资源被限制为完全不允许某些用户访问。有几个方法可以做到这一点。一个方法是加入应用逻辑到处理控制器或者视图的程序中,禁止某些用户访问。另一个方案是设置运行时的系统,对于一些资源,仅允许经由另一个应用资源内部调用。在这种情形,对于这些资源的访问必须被通过另一个表现层的应用资源进行,例如一个servlet控制器。对于这些受限制的资源不允许通过一个浏览器直接调用。
处理这个问题的一个常见方法是使用一个控制器来作为该类访问控制的一个委托者。另一个常见的方式是在一个视图中置入一个保护设置。我们这里主要讨论基于视图的控制策略。在考虑选择何种方式来控制访问之前,我们首先来描述一下这些策略。
在视图中置入保护逻辑
对于在一个视图的处理中置入一个保护逻辑,有两个常见的应用。一个是防止访问整个的资源,而另一个是限制访问部分的资源。
在每个视图中包含一个All-or-Nothing保护
在一些情况下,置入到视图处理代码中的逻辑以all-or-nothing的模式允许或者拒绝访问。也就是说,这个逻辑限制某个特别的用户访问一个特别的视图。通常这一类型的保护最好封装到一个中央化的控制器中,这样便于集中化管理。如果只有很少的页面需要防护,那么可以使用这个策略。通常这个情形都是发生在一个非技术人员需要更新网站一小部分的静态文件。如果客户仍然需要登陆到网站来浏览这些页面,那么只需要在每个页面的顶部加入一个自定义的tag(标记)就可以做到控制访问。如3.1的例子所示。
例子3.1 在每个视图中包含一个All-or-Nothing保护
<%@ taglib uri="/WEB-INF/corej2eetaglibrary.tld" prefix="corePatterns" %> <corePatterns:guard/> <HTML> . . . </HTML> |
给视图的某些部分加入保护
在其它情况下,置入到视图处理代码的逻辑可拒绝访问一个视图的某些部分。这个策略可以和上面的all-or-nothing策略一起使用。为说明这一点,我们这里使用控制访问一个建筑物中的一个房间作类比。all-or-nothing的保护策略告诉用户是否可以进入房间,而第二个保护策略则是告诉用户在进入房间后,允许他们看到什么东西。以下就是一些你可以利用这个策略的例子。
根据用户的角色决定是否显示视图的某些部分
根据用户的角色,视图的某部分可能不显示。例如,一个经理在收看管理信息时,他可以访问到其员工的子视图,而作为一个员工,他只可以看到自己组织的信息,而不可以访问其它信息,如例子3.2所示。
例子3.2 根据用户的角色,部分的视图不显示
<%@ taglib uri="/WEB-INF/corej2eetaglibrary.tld" prefix="corePatterns" %> <HTML> . . . <corePatterns:guard role="manager"> <b>This should be seen only by managers!</b> <corePatterns:guard/> . . . </HTML> |
根据系统的状态或者错误情形不显示部分的视图
根据系统的环境,显示的规划可以被修改。例如,如果用户使用的是一个单CPU的硬件设备,那么使用多个CPU的部分设备就可以不显示。
根据配置控制资源访问
要限制某个客户直接访问一个特别的视图,你可以配置表现层只有通过内部的资源才可以访问到这些资源,例如一个使用RequestDispatcher的servlet控制器。此外,你还可以使用Web容器中内置
的安全技术,根据servlet2.2或者以后的规范。安全限制被定义在称为web.xml的配置描述文件中(deployment descriptor)。
basic和form-based的认证方法在Servlet规范中也有描述。在此我们不打算重复这个规范,你可以到以下网址去查看当前规范的细节(http://java.sun.com/products/servlet/index.html)。
你已经明白了加入安全限制到你的应用时会有什么用处,我们简要讨论了这个问题并且介绍了如何通过配置令它和all-or-nothing保护相关。最后,我们描述了一个简单和常用的方法作为all-or-nothing保护,以限制一个资源的访问。
通过安全限制保护资源
应用或许被配置在一个安全限制中,而这个安全限制允许使用编程的方法根据用户的角色来控制访问。资源可以被某些角色的用户访问,并且禁止其它的角色访问。另外,某个视图的一部分也可以根据用户的角色来限制访问。如果某些资源完全禁止全部的直接浏览器请求,例如上面all-or-nothing情景中提到的一样,那么这些资源可以只允许一些安全角色访问,而这些安全角色不分配给任何一个用户。这样只要不分配这个安全角色,那么以这种方式配置的资源将禁止所有的浏览器直接访问。例子3.3就是一个web.xml配置文件的一部分,它定义了一个安全的角色以限制直接的浏览器访问。
角色的名字是“sensitive”,受限制资源的名字是sensitive1.jsp, sensitive2.jsp和sensitive3.jsp。除非一个用户或者组被分配到“sensitive”角色,否则这些客户都不可以直接访问这些JSP页面。不过,由于内部的请求并不受这些安全的限制,一个初始时由某servlet控制器处理的请求将会导向到这些受限制的页面,这样它们就可以间接访问这些JSP页面。
最后,要注意的是,不同厂家的产品在实现Servlet2.2版本的规范时,在这个方面有些不兼容的现象。不过支持Servlet2.3的服务器则没有这个兼容性的问题。
例子 3.3 通过不分配安全角色提供All-or-Nothing控制
<security-constraint> <web-resource-collection> <web-resource-name>SensitiveResources </web-resource-name> <description>A Collection of Sensitive Resources </description> <url-pattern>/trade/jsp/internalaccess/ sensitive1.jsp</url-pattern> <url-pattern>/trade/jsp/internalaccess/ sensitive2.jsp</url-pattern> <url-pattern>/trade/jsp/internalaccess/ sensitive3.jsp</url-pattern> <http-method>GET</http-method> <http-method>POST</http-method> </web-resource-collection> <auth-constraint> <role-name>sensitive</role-name> </auth-constraint> </security-constraint> |
设置资源保护的一个简单方法
有一个简单和常见的方法可以限制一个客户直接访问某个资源,例如JSP。和3.3的例子一样,这个方法无需修改任何的配置文件。这个方法只是需要将资源放置在Web应用的/WEB-INF/目录下。例如,要防止浏览器直接访问一个称为info.jsp的视图,我们假设这个文件是属于一个名字为securityissues的Web应用。我们可以将该JSP文件放在以下的子目录:/securityissues/WEB-INF/internalaccessonly/info.jsp。
对于/WEB-INF/目录及其子目录是禁止浏览器直接访问的,因此info.jsp也不可以直接访问。不过,如果需要,一个控制器servlet仍然可以导向到这个资源。这种控制使用的是all-or-nothing的方式,因为以这种方式配置的资源都完全禁止浏览器直接访问。
重复的Form提交
用户使用浏览器时,可以经常使用向后的按钮,因此就有可能重复提交一个他们已经提交过的form,这样就会带来一个重复事务处理的问题。同样,一个用户也可能在接收到一个确认的页面之前按下停止的按钮,接着再次提交同一个form。对于这些情况,我们都想跟踪并且禁止这些重复的提交,我们可以使用一个控制servlet来提供一个控制点,以解决这个问题。
同步记号(Synchronizer (or Dvu) Token)
这个策略是为了解决重复的form提交问题。一个同步的记号被设置在一个用户的Session中,并且包含在返回到客户的每一个form中。当form被提交时,form中的同步标记就和Session中的同步标记作对比。在form首次提交的时候,这两个标记应该是一样的。如果标记不一样,那么该form就会禁止提交,一个错误就会返回给用户。在用户提交一个form时,如果按下浏览器中的后退按钮并尝试重新提交同一个form时,标记就会出现不匹配的现象。
另一方面,如果两个标记值匹配,那么我们就可以确信整个流程是正确的。在这种情况下,Session中的标记值就会被修改为一个新的值,同时允许提交该form。
你也可以使用这个策略来控制对某些页面的直接访问,就好象上面资源保护中描述的一样。例如,假设一个用户将某个应用的页面A收藏到收藏夹中,而页面A只允许通过页面B和C访问。当用户直接通过收藏夹来访问页面A,这时页面的访问顺序就是不正确的,这样同步标记将处在一个不同步的状态,或者它根本就不存在。不论怎样,访问都被禁止了。
验证
通常我们都希望同时在客户和服务器端进行验证。虽然客户端的验证处理看来没有服务器端验证那样专业,不过它可以提供高级别的检查,例如验证form中的某个字段是否为空。服务器端的验证通常要广泛得多。虽然在一个应用中,两种类型的处理都是适当的,不过这里不建议只使用客户端的验证。这样做的一个主要原因是由于客户端的验证是使用客户端的脚本语言的,用户可以在任何时候通过设置,从而跳过这些脚本。
这里不打算很详细地讨论验证的策略。我们只是在这里提及一下这是一个在设计系统时需要考虑到的问题,如果你想更深入地了解,你可以参考现有的文献。
在客户端验证
输入的验证在客户端进行。通常这个操作都是通过嵌入的脚本代码例如JavaScript进行。上面已经提到,客户端的验证是服务器端验证的一个补充,不过不应该独立使用。
在服务器端验证
输入的验证在服务器端进行。有几个典型的策略可用作服务器端验证。这些策略是Form-Centric Validation(以Form为中心的验证)和validation based on abstract types(基于抽象类型的验证)。
Form-Centric Validation
Form-Centric Validation的策略强迫一个应用包含许多的方法来验证form各部分的状态。典型地,这些方法依据其包含的逻辑,都是交迭的,这样不利于重新使用和模块化。一个验证的方法都是和用户某个特定的form相关,没有共同的代码来处理常见的操作,例如必填的字段或者只可以使用数字的字段。在这种情况下,当某个字段被用在多个不同的form,并且是一个必填的字段时,它都是在应用中独立处理的。这个策略实现起来相对容易,并且效率也高,不过当应用变大时,就会带来重复代码的问题。
要提供一个更灵活的、可重用的和易于维护的方法,数据模型应该作更大的抽象。这个方式就是下面提到的“根据抽象类型来验证,form-centric验证的例子见例子3.4。
Example 3.4 Form-Centric Validation /**If the first name or last name fields were left blank, then an error will be returned to client. With this strategy, these checks for the existence of a required field are duplicated. If this valid- ation logic were abstracted into a separate component, it could be reused across forms (see Validation Based on Abstract Types strategy)**/ public Vector validate() { Vector errorCollection = new Vector(); if ((firstname == null) || (firstname.trim.length() < 1)) errorCollection.addElement("firstname required"); if ((lastname == null) || (lastname.trim.length() < 1)) errorCollection.addElement("lastname required"); return errorCollection; } |