- 浏览: 26745 次
- 性别:
- 来自: 广州
文章分类
最新评论
-
lucky16:
今天刚好看看notnull 和notblank的区别。。。留着 ...
Hibernate Validator -
悲剧了:
哥们专注请注明出处http://elf8848.iteye.c ...
Spring 3 MVC深入研究
在一个有密码保护的Web应用中,正确处理用户退出过程并不仅仅只需调用HttpSession的invalidate()方法。现在大部分浏览器上都有后退和前进按钮,允许用户后退或前进到一个页面。如果在用户在退出一个Web应用后按了后退按钮浏览器把缓存中的页面呈现给用户,这会使用户产生疑惑,他们会开始担心他们的个人数据是否安全。许多Web应用强迫用户退出时关闭整个浏览器,这样,用户就无法点击后退按钮了。还有一些使用javascript,但在某些客户端浏览器这却不一定起作用。这些解决方案都很笨拙且不能保证在任一情况下100%有效,同时,它也要求用户有一定的操作经验。
这篇文章以示例阐述了正确解决用户退出问题的方案。作者Kevin Le首先描述了一个密码保护Web应用,然后以示例程序解释问题如何产生并讨论解决问题的方案。文章虽然是针对JSP页面进行阐述,但作者所阐述的概念很容易理解切能够为其他Web技术所采用。最后作者展示了如何用Jakarta Struts优雅地解决这一问题。
大部分Web应用不会包含象银行账户或信用卡资料那样机密的信息,但一旦涉及到敏感数据,我们就需要提供一类密码保护机制。举例来说,一个工厂中工人通过Web访问他们的时间安排、进入他们的训练课程以及查看他们的薪金等等。此时应用SSL(Secure Socket Layer)有点杀鸡用牛刀的感觉,但不可否认,我们又必须为这些应用提供密码保护,否则,工人(也就是Web应用的使用者)可以窥探到工厂中其他雇员的私人机密信息。
与上述情形相似的还有位处图书馆、医院等公共场所的计算机。在这些地方,许多用户共同使用几台计算机,此时保护用户的个人数据就显得至关重要。设计良好编写优秀的应用对用户专业知识的要求少之又少。
我们来看一下现实世界中一个完美的Web应用是如何表现的:一个用户通过浏览器访问一个页面。Web应用展现一个登陆页面要求用户输入有效的验证信息。用户输入了用户名和密码。此时我们假设用户提供的身份验证信息是正确的,经过了验证过程,Web应用允许用户浏览他有权访问的区域。用户想退出时,点击退出按钮,Web应用要求用户确认他是否则真的需要退出,如果用户确定退出,Session结束,Web应用重新定位到登陆页面。用户可以放心的离开而不用担心他的信息会泄露。另一个用户坐到了同一台电脑前,他点击后退按钮,Web应用不应该出现上一个用户访问过的任何一个页面。事实上,Web应用在第二个用户提供正确的验证信息之前应当一直停留在登陆页面上。
通过示例程序,文章向您阐述了如何在一个Web应用中实现这一功能。
JSP示例
为了更为有效地阐述实现方案,本文将从展示一个示例应用logoutSampleJSP1中碰到的问题开始。这个示例代表了许多没有正确解决退出过程的Web应用。logoutSampleJSP1包含了下述jsp页面:login.jsp, home.jsp, secure1.jsp, secure2.jsp, logout.jsp, loginAction.jsp, and logoutAction.jsp。其中页面home.jsp, secure1.jsp, secure2.jsp, 和logout.jsp是不允许未经认证的用户访问的,也就是说,这些页面包含了重要信息,在用户登陆之前或者退出之后都不应该出现在浏览器中。login.jsp包含了用于用户输入用户名和密码的form。logout.jsp页包含了要求用户确认是否退出的form。loginAction.jsp和logoutAction.jsp作为控制器分别包含了登陆和退出代码。
第二个示例应用logoutSampleJSP2展示了如何解决示例logoutSampleJSP1中的问题。然而,第二个应用自身也是有疑问的。在特定的情况下,退出问题还是会出现。
第三个示例应用logoutSampleJSP3在第二个示例上进行了改进,比较完善地解决了退出问题。
最后一个示例logoutSampleStruts展示了Struts如何优美地解决登陆问题。
注意:本文所附示例在最新版本的Microsoft Internet Explorer (IE), Netscape Navigator, Mozilla, FireFox和Avant浏览器上测试通过。
Login action
Brian Pontarelli的经典文章《J2EE Security: Container Versus Custom》讨论了不同的J2EE认证途径。文章同时指出,HTTP协议和基于form的认证并未提供处理用户退出的机制。因此,解决途径便是引入自定义的安全实现机制。
自定义的安全认证机制普遍采用的方法是从form中获得用户输入的认证信息,然后到诸如LDAP (lightweight directory access protocol)或关系数据库的安全域中进行认证。如果用户提供的认证信息是有效的,登陆动作往HttpSession对象中注入某个对象。HttpSession存在着注入的对象则表示用户已经登陆。为了方便读者理解,本文所附的示例只往HttpSession中写入一个用户名以表明用户已经登陆。清单1是从loginAction.jsp页面中节选的一段代码以此阐述登陆动作:
Listing 1
//...
//initialize RequestDispatcher object; set forward to home page by default
RequestDispatcher rd = request.getRequestDispatcher("home.jsp");
//Prepare connection and statement
rs = stmt.executeQuery("select password from USER where userName = '" + userName + "'");
if (rs.next()) {
//Query only returns 1 record in the result set; only 1
password per userName which is also the primary key
if (rs.getString("password").equals(password)) { //If valid password
session.setAttribute("User", userName); //Saves username string in the session object
}
else { //Password does not match, i.e., invalid user password
request.setAttribute("Error", "Invalid password.");
rd = request.getRequestDispatcher("login.jsp");
}
} //No record in the result set, i.e., invalid username
else {
request.setAttribute("Error", "Invalid user name.");
rd = request.getRequestDispatcher("login.jsp");
}
}
//As a controller, loginAction.jsp finally either forwards to "login.jsp" or "home.jsp"
rd.forward(request, response);
//...
本文所附示例均以关系型数据库作为安全域,但本文所阐述的观点对任何类型的安全域都是适用的。
Logout action
退出动作就包含了简单的删除用户名以及对用户的HttpSession对象调用invalidate()方法。清单2是从loginoutAction.jsp页面中节选的一段代码以此阐述退出动作:
Listing 2
//...
session.removeAttribute("User");
session.invalidate();
//...
阻止未经认证访问受保护的JSP页面
从form中获取用户提交的认证信息并经过验证后,登陆动作简单地往 HttpSession对象中写入一个用户名,退出动作则做相反的工作,它从用户的HttpSession对象中删除用户名并调用invalidate()方法销毁HttpSession。为了使登陆和退出动作真正发挥作用,所有受保护的JSP页面都应该首先验证HttpSession中是否包含了用户名以确认当前用户是否已经登陆。如果HttpSession中包含了用户名,也就是说用户已经登陆,Web应用则将剩余的JSP页发送给浏览器,否则,JSP页将跳转到登陆页login.jsp。页面home.jsp, secure1.jsp, secure2.jsp和logout.jsp均包含清单3中的代码段:
Listing 3
//...
String userName = (String) session.getAttribute("User");
if (null == userName) {
request.setAttribute("Error", "Session has ended. Please login.");
RequestDispatcher rd = request.getRequestDispatcher("login.jsp");
rd.forward(request, response);
}
//...
//Allow the rest of the dynamic content in this JSP to be served to the browser
//...
在这个代码段中,程序从HttpSession中减缩username字符串。如果字符串为空,Web应用则自动中止执行当前页面并跳转到登陆页,同时给出Session has ended. Please log in.的提示;如果不为空,Web应用则继续执行,也就是把剩余的页面提供给用户。
运行logoutSampleJSP1
运行logoutSampleJSP1将会出现如下几种情形:
1) 如果用户没有登陆,Web应用将会正确中止受保护页面home.jsp, secure1.jsp, secure2.jsp和logout.jsp的执行,也就是说,假如用户在浏览器地址栏中直接敲入受保护JSP页的地址试图访问,Web应用将自动跳转到登陆页并提示Session has ended.Please log in.
2) 同样的,当一个用户已经退出,Web应用也会正确中止受保护页面home.jsp, secure1.jsp, secure2.jsp和logout.jsp的执行
3) 用户退出后,如果点击浏览器上的后退按钮,Web应用将不能正确保护受保护的页面——在Session销毁后(用户退出)受保护的JSP页重新在浏览器中显示出来。然而,如果用户点击返回页面上的任何链接,Web应用将会跳转到登陆页面并提示Session has ended.Please log in.
阻止浏览器缓存
上述问题的根源在于大部分浏览器都有一个后退按钮。当点击后退按钮时,默认情况下浏览器不是从Web服务器上重新获取页面,而是从浏览器缓存中载入页面。基于Java的Web应用并未限制这一功能,在基于PHP、ASP和.NET的Web应用中也同样存在这一问题。
在用户点击后退按钮后,浏览器到服务器再从服务器到浏览器这样通常意思上的HTTP回路并没有建立,仅仅只是用户,浏览器和缓存进行了交互。所以,即使包含了清单3上的代码来保护JSP页面,当点击后退按钮时,这些代码是不会执行的。
缓存的好坏,真是仁者见仁智者见智。缓存的确提供了一些便利,但通常只在使用静态的HTML页面或基于图形或影响的页面你才能感受到。而另一方面,Web应用通常是基于数据的,数据通常是频繁更改的。与从缓存中读取并显示过期的数据相比,提供最新的数据才是更重要的!
幸运的是,HTTP头信息“Expires”和“Cache-Control”为应用程序服务器提供了一个控制浏览器和代理服务器上缓存的机制。HTTP头信息Expires告诉代理服务器它的缓存页面何时将过期。HTTP1.1规范中新定义的头信息Cache-Control可以通知浏览器不缓存任何页面。当点击后退按钮时,浏览器重新访问服务器已获取页面。如下是使用Cache-Control的基本方法:
1) no-cache:强制缓存从服务器上获取新的页面
2) no-store: 在任何环境下缓存不保存任何页面
HTTP1.0规范中的Pragma:no-cache等同于HTTP1.1规范中的Cache-Control:no-cache,同样可以包含在头信息中。
通过使用HTTP头信息的cache控制,第二个示例应用logoutSampleJSP2解决了logoutSampleJSP1的问题。logoutSampleJSP2与logoutSampleJSP1不同表现在如下代码段中,这一代码段加入进所有受保护的页面中:
//...
response.setHeader("Cache-Control","no-cache"); //Forces caches to obtain a new copy of the page from the origin server
response.setHeader("Cache-Control","no-store"); //Directs caches not to store the page under any circumstance
response.setDateHeader("Expires", 0); //Causes the proxy cache to see the page as "stale"
response.setHeader("Pragma","no-cache"); //HTTP 1.0 backward compatibility
String userName = (String) session.getAttribute("User");
if (null == userName) {
request.setAttribute("Error", "Session has ended. Please login.");
RequestDispatcher rd = request.getRequestDispatcher("login.jsp");
rd.forward(request, response);
}
//...
通过设置头信息和检查HttpSession中的用户名确保了浏览器不缓存页面,同时,如果用户未登陆,受保护的JSP页面将不会发送到浏览器,取而代之的将是登陆页面login.jsp。
运行logoutSampleJSP2
运行logoutSampleJSP2后将回看到如下结果:
1) 当用户退出后试图点击后退按钮,浏览器并不会显示受保护的页面,它只会现实登陆页login.jsp同时给出提示信息Session has ended. Please log in.
2) 然而,当按了后退按钮返回的页是处理用户提交数据的页面时,IE和Avant浏览器将弹出如下信息提示:
警告:页面已过期……(你肯定见过)
选择刷新后前一个JSP页面将重新显示在浏览器中。很显然,这不是我们所想看到的因为它违背了logout动作的目的。发生这一现象时,很可能是一个恶意用户在尝试获取其他用户的数据。然而,这个问题仅仅出现在后退按钮对应的是一个处理POST请求的页面。
记录最后登陆时间
上述问题之所以出现是因为浏览器将其缓存中的数据重新提交了。这本文的例子中,数据包含了用户名和密码。无论是否给出安全警告信息,浏览器此时起到了负面作用。
为了解决logoutSampleJSP2中出现的问题,logoutSampleJSP3的login.jsp在包含username和password的基础上还包含了一个称作lastLogon的隐藏表单域,此表单域动态的用一个long型值初始化。这个long型值是调用System.currentTimeMillis()获取到的自1970年1月1日以来的毫秒数。当login.jsp中的form提交时,loginAction.jsp首先将隐藏域中的值与用户数据库中的值进行比较。只有当lastLogon表单域中的值大于数据库中的值时Web应用才认为这是个有效的登陆。
为了验证登陆,数据库中lastLogon字段必须以表单中的lastLogon值进行更新。上例中,当浏览器重复提交数据时,表单中的lastLogon值不比数据库中的lastLogon值大,因此,loginAction转到login.jsp页面,并提示Session has ended.Please log in.清单5是loginAction中节选的代码段:
清单5
//...
RequestDispatcher rd = request.getRequestDispatcher("home.jsp"); //Forward to homepage by default
//...
if (rs.getString("password").equals(password)) {
//If valid password
long lastLogonDB = rs.getLong("lastLogon");
if (lastLogonForm > lastLogonDB) {
session.setAttribute("User", userName); //Saves username string in the session object
stmt.executeUpdate("update USER set lastLogon= " + lastLogonForm + " where userName = '" + userName + "'");
}
else {
request.setAttribute("Error", "Session has ended. Please login.");
rd = request.getRequestDispatcher("login.jsp"); }
}
else { //Password does not match, i.e., invalid user password
request.setAttribute("Error", "Invalid password.");
rd = request.getRequestDispatcher("login.jsp");
}
//...
rd.forward(request, response);
//...
为了实现上述方法,你必须记录每个用户的最后登陆时间。对于采用关系型数据库安全域来说,这点可以可以通过在某个表中加上lastLogin字段轻松实现。LDAP以及其他的安全域需要稍微动下脑筋,但很显然是可以实现的。
表示最后登陆时间的方法有很多。示例logoutSampleJSP3利用了自1970年1月1日以来的毫秒数。这个方法在许多人在不同浏览器中用一个用户账号登陆时也是可行的。
运行logoutSampleJSP3
运行示例logoutSampleJSP3将展示如何正确处理退出问题。一旦用户退出,点击浏览器上的后退按钮在任何情况下都不会是受保护的页面在浏览器上显示出来。这个示例展示了如何正确处理退出问题而不需要额外的培训。
为了使代码更简练有效,一些冗余的代码可以剔除掉。一种途径就是把清单4中的代码写到一个单独的JSP页中,通过标签<jsp:include>其他页面也可以引用。
Struts框架下的退出实现
与直接使用JSP或JSP/servlets相比,另一个可选的方案是使用Struts。为一个基于Struts的Web应用添加一个处理退出问题的框架可以优雅地不费气力的实现。这部分归功于Struts是采用MVC设计模式的因此将模型和视图清晰的分开。另外,Java是一个面向对象的语言,其支持继承,可以比JSP中的脚本更为容易地实现代码重用。在Struts中,清单4中的代码可以从JSP页面中移植到Action类的execute()方法中。
此外,我们还可以定义一个继承Struts Action类的基本类,其execute()方法中包含了清单4中的代码。通过使用类继承机制,其他类可以继承基本类中的通用逻辑来设置HTTP头信息以及检索HttpSession对象中的username字符串。这个基本类是一个抽象类并定义了一个抽象方法executeAction()。所有继承自基类的子类都应实现exectuteAction()方法而不是覆盖它。清单6是基类的部分代码:
清单6
public abstract class BaseAction extends Action {
public ActionForward execute(ActionMapping mapping, ActionForm form,HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
response.setHeader("Cache-Control","no-cache");
//Forces caches to obtain a new copy of the page from the origin server
response.setHeader("Cache-Control","no-store");
//Directs caches not to store the page under any circumstance
response.setDateHeader("Expires", 0); //Causes the proxy cache to see the page as "stale"
response.setHeader("Pragma","no-cache"); //HTTP 1.0 backward compatibility
if (!this.userIsLoggedIn(request)) {
ActionErrors errors = new ActionErrors();
errors.add("error", new ActionError("logon.sessionEnded"));
this.saveErrors(request, errors);
return mapping.findForward("sessionEnded");
}
return executeAction(mapping, form, request, response);
}
protected abstract ActionForward executeAction(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException;
private boolean userIsLoggedIn(HttpServletRequest request) {
if (request.getSession().getAttribute("User") == null) {
return false;
}
return true;
}
}
清单6中的代码与清单4中的很相像,仅仅只是用ActionMapping findForward替代了RequestDispatcher forward。清单6中,如果在HttpSession中未找到username字符串,ActionMapping对象将找到名为sessionEnded的forward元素并跳转到对应的path。如果找到了,子类将执行其实现了executeAction()方法的业务逻辑。因此,在配置文件struts-web.xml中为所有子类声明个一名为sessionEnded的forward元素是必须的。清单7以secure1 action阐明了这样一个声明:
清单7
<action path="/secure1"
type="com.kevinhle.logoutSampleStruts.Secure1Action"
scope="request">
<forward name="success" path="/WEB-INF/jsps/secure1.jsp"/>
<forward name="sessionEnded" path="/login.jsp"/>
</action>
继承自BaseAction类的子类Secure1Action实现了executeAction()方法而不是覆盖它。Secure1Action类不执行任何退出代码,如清单8:
public class Secure1Action extends BaseAction {
public ActionForward executeAction(ActionMapping mapping, ActionForm form,
HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
HttpSession session = request.getSession();
return (mapping.findForward("success"));
}
}
只需要定义一个基类而不需要额外的代码工作,上述解决方案是优雅而有效的。不管怎样,将通用的行为方法写成一个继承StrutsAction的基类是许多Struts项目的共同经验,值得推荐。
结论
本文阐述了解决退出问题的方案,尽管方案简单的令人惊讶,但却在所有情况下都能有效地工作。无论是对JSP还是Struts,所要做的不过是写一段不超过50行的代码以及一个记录用户最后登陆时间的方法。在Web应用中混合使用这些方案能够使拥护的私人数据不致泄露,同时,也能增加用户的经验。
转载自:http://hi.baidu.com/beer_zh/blog/item/6991f838503d29e8b311c703.html
这篇文章以示例阐述了正确解决用户退出问题的方案。作者Kevin Le首先描述了一个密码保护Web应用,然后以示例程序解释问题如何产生并讨论解决问题的方案。文章虽然是针对JSP页面进行阐述,但作者所阐述的概念很容易理解切能够为其他Web技术所采用。最后作者展示了如何用Jakarta Struts优雅地解决这一问题。
大部分Web应用不会包含象银行账户或信用卡资料那样机密的信息,但一旦涉及到敏感数据,我们就需要提供一类密码保护机制。举例来说,一个工厂中工人通过Web访问他们的时间安排、进入他们的训练课程以及查看他们的薪金等等。此时应用SSL(Secure Socket Layer)有点杀鸡用牛刀的感觉,但不可否认,我们又必须为这些应用提供密码保护,否则,工人(也就是Web应用的使用者)可以窥探到工厂中其他雇员的私人机密信息。
与上述情形相似的还有位处图书馆、医院等公共场所的计算机。在这些地方,许多用户共同使用几台计算机,此时保护用户的个人数据就显得至关重要。设计良好编写优秀的应用对用户专业知识的要求少之又少。
我们来看一下现实世界中一个完美的Web应用是如何表现的:一个用户通过浏览器访问一个页面。Web应用展现一个登陆页面要求用户输入有效的验证信息。用户输入了用户名和密码。此时我们假设用户提供的身份验证信息是正确的,经过了验证过程,Web应用允许用户浏览他有权访问的区域。用户想退出时,点击退出按钮,Web应用要求用户确认他是否则真的需要退出,如果用户确定退出,Session结束,Web应用重新定位到登陆页面。用户可以放心的离开而不用担心他的信息会泄露。另一个用户坐到了同一台电脑前,他点击后退按钮,Web应用不应该出现上一个用户访问过的任何一个页面。事实上,Web应用在第二个用户提供正确的验证信息之前应当一直停留在登陆页面上。
通过示例程序,文章向您阐述了如何在一个Web应用中实现这一功能。
JSP示例
为了更为有效地阐述实现方案,本文将从展示一个示例应用logoutSampleJSP1中碰到的问题开始。这个示例代表了许多没有正确解决退出过程的Web应用。logoutSampleJSP1包含了下述jsp页面:login.jsp, home.jsp, secure1.jsp, secure2.jsp, logout.jsp, loginAction.jsp, and logoutAction.jsp。其中页面home.jsp, secure1.jsp, secure2.jsp, 和logout.jsp是不允许未经认证的用户访问的,也就是说,这些页面包含了重要信息,在用户登陆之前或者退出之后都不应该出现在浏览器中。login.jsp包含了用于用户输入用户名和密码的form。logout.jsp页包含了要求用户确认是否退出的form。loginAction.jsp和logoutAction.jsp作为控制器分别包含了登陆和退出代码。
第二个示例应用logoutSampleJSP2展示了如何解决示例logoutSampleJSP1中的问题。然而,第二个应用自身也是有疑问的。在特定的情况下,退出问题还是会出现。
第三个示例应用logoutSampleJSP3在第二个示例上进行了改进,比较完善地解决了退出问题。
最后一个示例logoutSampleStruts展示了Struts如何优美地解决登陆问题。
注意:本文所附示例在最新版本的Microsoft Internet Explorer (IE), Netscape Navigator, Mozilla, FireFox和Avant浏览器上测试通过。
Login action
Brian Pontarelli的经典文章《J2EE Security: Container Versus Custom》讨论了不同的J2EE认证途径。文章同时指出,HTTP协议和基于form的认证并未提供处理用户退出的机制。因此,解决途径便是引入自定义的安全实现机制。
自定义的安全认证机制普遍采用的方法是从form中获得用户输入的认证信息,然后到诸如LDAP (lightweight directory access protocol)或关系数据库的安全域中进行认证。如果用户提供的认证信息是有效的,登陆动作往HttpSession对象中注入某个对象。HttpSession存在着注入的对象则表示用户已经登陆。为了方便读者理解,本文所附的示例只往HttpSession中写入一个用户名以表明用户已经登陆。清单1是从loginAction.jsp页面中节选的一段代码以此阐述登陆动作:
Listing 1
//...
//initialize RequestDispatcher object; set forward to home page by default
RequestDispatcher rd = request.getRequestDispatcher("home.jsp");
//Prepare connection and statement
rs = stmt.executeQuery("select password from USER where userName = '" + userName + "'");
if (rs.next()) {
//Query only returns 1 record in the result set; only 1
password per userName which is also the primary key
if (rs.getString("password").equals(password)) { //If valid password
session.setAttribute("User", userName); //Saves username string in the session object
}
else { //Password does not match, i.e., invalid user password
request.setAttribute("Error", "Invalid password.");
rd = request.getRequestDispatcher("login.jsp");
}
} //No record in the result set, i.e., invalid username
else {
request.setAttribute("Error", "Invalid user name.");
rd = request.getRequestDispatcher("login.jsp");
}
}
//As a controller, loginAction.jsp finally either forwards to "login.jsp" or "home.jsp"
rd.forward(request, response);
//...
本文所附示例均以关系型数据库作为安全域,但本文所阐述的观点对任何类型的安全域都是适用的。
Logout action
退出动作就包含了简单的删除用户名以及对用户的HttpSession对象调用invalidate()方法。清单2是从loginoutAction.jsp页面中节选的一段代码以此阐述退出动作:
Listing 2
//...
session.removeAttribute("User");
session.invalidate();
//...
阻止未经认证访问受保护的JSP页面
从form中获取用户提交的认证信息并经过验证后,登陆动作简单地往 HttpSession对象中写入一个用户名,退出动作则做相反的工作,它从用户的HttpSession对象中删除用户名并调用invalidate()方法销毁HttpSession。为了使登陆和退出动作真正发挥作用,所有受保护的JSP页面都应该首先验证HttpSession中是否包含了用户名以确认当前用户是否已经登陆。如果HttpSession中包含了用户名,也就是说用户已经登陆,Web应用则将剩余的JSP页发送给浏览器,否则,JSP页将跳转到登陆页login.jsp。页面home.jsp, secure1.jsp, secure2.jsp和logout.jsp均包含清单3中的代码段:
Listing 3
//...
String userName = (String) session.getAttribute("User");
if (null == userName) {
request.setAttribute("Error", "Session has ended. Please login.");
RequestDispatcher rd = request.getRequestDispatcher("login.jsp");
rd.forward(request, response);
}
//...
//Allow the rest of the dynamic content in this JSP to be served to the browser
//...
在这个代码段中,程序从HttpSession中减缩username字符串。如果字符串为空,Web应用则自动中止执行当前页面并跳转到登陆页,同时给出Session has ended. Please log in.的提示;如果不为空,Web应用则继续执行,也就是把剩余的页面提供给用户。
运行logoutSampleJSP1
运行logoutSampleJSP1将会出现如下几种情形:
1) 如果用户没有登陆,Web应用将会正确中止受保护页面home.jsp, secure1.jsp, secure2.jsp和logout.jsp的执行,也就是说,假如用户在浏览器地址栏中直接敲入受保护JSP页的地址试图访问,Web应用将自动跳转到登陆页并提示Session has ended.Please log in.
2) 同样的,当一个用户已经退出,Web应用也会正确中止受保护页面home.jsp, secure1.jsp, secure2.jsp和logout.jsp的执行
3) 用户退出后,如果点击浏览器上的后退按钮,Web应用将不能正确保护受保护的页面——在Session销毁后(用户退出)受保护的JSP页重新在浏览器中显示出来。然而,如果用户点击返回页面上的任何链接,Web应用将会跳转到登陆页面并提示Session has ended.Please log in.
阻止浏览器缓存
上述问题的根源在于大部分浏览器都有一个后退按钮。当点击后退按钮时,默认情况下浏览器不是从Web服务器上重新获取页面,而是从浏览器缓存中载入页面。基于Java的Web应用并未限制这一功能,在基于PHP、ASP和.NET的Web应用中也同样存在这一问题。
在用户点击后退按钮后,浏览器到服务器再从服务器到浏览器这样通常意思上的HTTP回路并没有建立,仅仅只是用户,浏览器和缓存进行了交互。所以,即使包含了清单3上的代码来保护JSP页面,当点击后退按钮时,这些代码是不会执行的。
缓存的好坏,真是仁者见仁智者见智。缓存的确提供了一些便利,但通常只在使用静态的HTML页面或基于图形或影响的页面你才能感受到。而另一方面,Web应用通常是基于数据的,数据通常是频繁更改的。与从缓存中读取并显示过期的数据相比,提供最新的数据才是更重要的!
幸运的是,HTTP头信息“Expires”和“Cache-Control”为应用程序服务器提供了一个控制浏览器和代理服务器上缓存的机制。HTTP头信息Expires告诉代理服务器它的缓存页面何时将过期。HTTP1.1规范中新定义的头信息Cache-Control可以通知浏览器不缓存任何页面。当点击后退按钮时,浏览器重新访问服务器已获取页面。如下是使用Cache-Control的基本方法:
1) no-cache:强制缓存从服务器上获取新的页面
2) no-store: 在任何环境下缓存不保存任何页面
HTTP1.0规范中的Pragma:no-cache等同于HTTP1.1规范中的Cache-Control:no-cache,同样可以包含在头信息中。
通过使用HTTP头信息的cache控制,第二个示例应用logoutSampleJSP2解决了logoutSampleJSP1的问题。logoutSampleJSP2与logoutSampleJSP1不同表现在如下代码段中,这一代码段加入进所有受保护的页面中:
//...
response.setHeader("Cache-Control","no-cache"); //Forces caches to obtain a new copy of the page from the origin server
response.setHeader("Cache-Control","no-store"); //Directs caches not to store the page under any circumstance
response.setDateHeader("Expires", 0); //Causes the proxy cache to see the page as "stale"
response.setHeader("Pragma","no-cache"); //HTTP 1.0 backward compatibility
String userName = (String) session.getAttribute("User");
if (null == userName) {
request.setAttribute("Error", "Session has ended. Please login.");
RequestDispatcher rd = request.getRequestDispatcher("login.jsp");
rd.forward(request, response);
}
//...
通过设置头信息和检查HttpSession中的用户名确保了浏览器不缓存页面,同时,如果用户未登陆,受保护的JSP页面将不会发送到浏览器,取而代之的将是登陆页面login.jsp。
运行logoutSampleJSP2
运行logoutSampleJSP2后将回看到如下结果:
1) 当用户退出后试图点击后退按钮,浏览器并不会显示受保护的页面,它只会现实登陆页login.jsp同时给出提示信息Session has ended. Please log in.
2) 然而,当按了后退按钮返回的页是处理用户提交数据的页面时,IE和Avant浏览器将弹出如下信息提示:
警告:页面已过期……(你肯定见过)
选择刷新后前一个JSP页面将重新显示在浏览器中。很显然,这不是我们所想看到的因为它违背了logout动作的目的。发生这一现象时,很可能是一个恶意用户在尝试获取其他用户的数据。然而,这个问题仅仅出现在后退按钮对应的是一个处理POST请求的页面。
记录最后登陆时间
上述问题之所以出现是因为浏览器将其缓存中的数据重新提交了。这本文的例子中,数据包含了用户名和密码。无论是否给出安全警告信息,浏览器此时起到了负面作用。
为了解决logoutSampleJSP2中出现的问题,logoutSampleJSP3的login.jsp在包含username和password的基础上还包含了一个称作lastLogon的隐藏表单域,此表单域动态的用一个long型值初始化。这个long型值是调用System.currentTimeMillis()获取到的自1970年1月1日以来的毫秒数。当login.jsp中的form提交时,loginAction.jsp首先将隐藏域中的值与用户数据库中的值进行比较。只有当lastLogon表单域中的值大于数据库中的值时Web应用才认为这是个有效的登陆。
为了验证登陆,数据库中lastLogon字段必须以表单中的lastLogon值进行更新。上例中,当浏览器重复提交数据时,表单中的lastLogon值不比数据库中的lastLogon值大,因此,loginAction转到login.jsp页面,并提示Session has ended.Please log in.清单5是loginAction中节选的代码段:
清单5
//...
RequestDispatcher rd = request.getRequestDispatcher("home.jsp"); //Forward to homepage by default
//...
if (rs.getString("password").equals(password)) {
//If valid password
long lastLogonDB = rs.getLong("lastLogon");
if (lastLogonForm > lastLogonDB) {
session.setAttribute("User", userName); //Saves username string in the session object
stmt.executeUpdate("update USER set lastLogon= " + lastLogonForm + " where userName = '" + userName + "'");
}
else {
request.setAttribute("Error", "Session has ended. Please login.");
rd = request.getRequestDispatcher("login.jsp"); }
}
else { //Password does not match, i.e., invalid user password
request.setAttribute("Error", "Invalid password.");
rd = request.getRequestDispatcher("login.jsp");
}
//...
rd.forward(request, response);
//...
为了实现上述方法,你必须记录每个用户的最后登陆时间。对于采用关系型数据库安全域来说,这点可以可以通过在某个表中加上lastLogin字段轻松实现。LDAP以及其他的安全域需要稍微动下脑筋,但很显然是可以实现的。
表示最后登陆时间的方法有很多。示例logoutSampleJSP3利用了自1970年1月1日以来的毫秒数。这个方法在许多人在不同浏览器中用一个用户账号登陆时也是可行的。
运行logoutSampleJSP3
运行示例logoutSampleJSP3将展示如何正确处理退出问题。一旦用户退出,点击浏览器上的后退按钮在任何情况下都不会是受保护的页面在浏览器上显示出来。这个示例展示了如何正确处理退出问题而不需要额外的培训。
为了使代码更简练有效,一些冗余的代码可以剔除掉。一种途径就是把清单4中的代码写到一个单独的JSP页中,通过标签<jsp:include>其他页面也可以引用。
Struts框架下的退出实现
与直接使用JSP或JSP/servlets相比,另一个可选的方案是使用Struts。为一个基于Struts的Web应用添加一个处理退出问题的框架可以优雅地不费气力的实现。这部分归功于Struts是采用MVC设计模式的因此将模型和视图清晰的分开。另外,Java是一个面向对象的语言,其支持继承,可以比JSP中的脚本更为容易地实现代码重用。在Struts中,清单4中的代码可以从JSP页面中移植到Action类的execute()方法中。
此外,我们还可以定义一个继承Struts Action类的基本类,其execute()方法中包含了清单4中的代码。通过使用类继承机制,其他类可以继承基本类中的通用逻辑来设置HTTP头信息以及检索HttpSession对象中的username字符串。这个基本类是一个抽象类并定义了一个抽象方法executeAction()。所有继承自基类的子类都应实现exectuteAction()方法而不是覆盖它。清单6是基类的部分代码:
清单6
public abstract class BaseAction extends Action {
public ActionForward execute(ActionMapping mapping, ActionForm form,HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
response.setHeader("Cache-Control","no-cache");
//Forces caches to obtain a new copy of the page from the origin server
response.setHeader("Cache-Control","no-store");
//Directs caches not to store the page under any circumstance
response.setDateHeader("Expires", 0); //Causes the proxy cache to see the page as "stale"
response.setHeader("Pragma","no-cache"); //HTTP 1.0 backward compatibility
if (!this.userIsLoggedIn(request)) {
ActionErrors errors = new ActionErrors();
errors.add("error", new ActionError("logon.sessionEnded"));
this.saveErrors(request, errors);
return mapping.findForward("sessionEnded");
}
return executeAction(mapping, form, request, response);
}
protected abstract ActionForward executeAction(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException;
private boolean userIsLoggedIn(HttpServletRequest request) {
if (request.getSession().getAttribute("User") == null) {
return false;
}
return true;
}
}
清单6中的代码与清单4中的很相像,仅仅只是用ActionMapping findForward替代了RequestDispatcher forward。清单6中,如果在HttpSession中未找到username字符串,ActionMapping对象将找到名为sessionEnded的forward元素并跳转到对应的path。如果找到了,子类将执行其实现了executeAction()方法的业务逻辑。因此,在配置文件struts-web.xml中为所有子类声明个一名为sessionEnded的forward元素是必须的。清单7以secure1 action阐明了这样一个声明:
清单7
<action path="/secure1"
type="com.kevinhle.logoutSampleStruts.Secure1Action"
scope="request">
<forward name="success" path="/WEB-INF/jsps/secure1.jsp"/>
<forward name="sessionEnded" path="/login.jsp"/>
</action>
继承自BaseAction类的子类Secure1Action实现了executeAction()方法而不是覆盖它。Secure1Action类不执行任何退出代码,如清单8:
public class Secure1Action extends BaseAction {
public ActionForward executeAction(ActionMapping mapping, ActionForm form,
HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
HttpSession session = request.getSession();
return (mapping.findForward("success"));
}
}
只需要定义一个基类而不需要额外的代码工作,上述解决方案是优雅而有效的。不管怎样,将通用的行为方法写成一个继承StrutsAction的基类是许多Struts项目的共同经验,值得推荐。
结论
本文阐述了解决退出问题的方案,尽管方案简单的令人惊讶,但却在所有情况下都能有效地工作。无论是对JSP还是Struts,所要做的不过是写一段不超过50行的代码以及一个记录用户最后登陆时间的方法。在Web应用中混合使用这些方案能够使拥护的私人数据不致泄露,同时,也能增加用户的经验。
转载自:http://hi.baidu.com/beer_zh/blog/item/6991f838503d29e8b311c703.html
相关推荐
1. **禁用缓存**:在某些情况下,我们可能希望禁用浏览器缓存以测试服务器在处理无缓存请求时的性能。在LoadRunner中,可以通过在HTTP协议初始化函数(http_init())中设置`web_set_sockets_option("HDR_RESERVE", ...
### JS浏览器缓存详解 #### 一、问题背景与概述 在进行Web开发的过程中,开发者可能会遇到一个较为隐蔽但十分常见的问题——浏览器缓存导致的重复请求失效现象。特别是使用某些JavaScript库(如Prototype.js)时,...
### jsp去除浏览器缓存详解 #### 一、引言 在Web开发中,为了提高用户体验及页面加载速度,浏览器通常会缓存一部分资源。但有时为了确保用户获取到最新的内容,我们需要禁止或控制浏览器对特定页面的缓存。本文将...
ChromeCache.sln文件包含了项目的配置信息,而ChromeCache.suo则记录了用户的个性化设置,如断点、窗口位置等,它们并非直接用于处理浏览器缓存。 然而,"ChromeCache"通常指的是Chrome的磁盘缓存文件夹,位于用户...
在JSP中,通常有以下几种方法来处理浏览器缓存: 1. **元标签设置**: 在HTML的`<head>`部分,可以添加元标签来告诉浏览器不要缓存页面内容。例如: ```html ``` 这些标签分别设置了Pragma、Cache-...
本文将详细讲解浏览器缓存的工作原理,以及如何处理浏览器缓存问题。 首先,我们需要理解HTTP请求的基本流程。在TCP三次握手建立连接后,客户端通过HTTP请求获取服务器上的资源。浏览器会根据HTTP响应头中的Cache-...
Chrome浏览器缓存查看工具是一款专为用户设计的实用软件,旨在帮助用户轻松访问和管理Google Chrome浏览器中的缓存数据。这个工具允许用户查看并分析在浏览网页时存储的各种资源,如图片、音频、视频和文档等。对于...
在Web开发过程中,浏览器缓存是提升页面加载速度和用户体验的一个重要机制,但有时候它也会导致网页显示不及时更新的问题。为此,了解如何使用JavaScript来清除或避免浏览器缓存变得至关重要。本文介绍了几种使用...
服务器响应时包含`Expires` 或 `Cache-Control` 字段,以告知浏览器如何处理缓存。 2. **缓存检查**: 对于后续请求,浏览器首先检查资源是否仍然有效。如果没有过期,则直接从缓存中读取数据并返回200 OK (from ...
例如,设置Cache-Control为`no-cache`或`no-store`可以防止浏览器缓存页面,`must-revalidate`则要求每次请求都验证资源是否更新。 2. **HTML5的Service Worker**:Service Worker是一种在浏览器后台运行的脚本,它...
### 清除浏览器缓存的方法及原理 #### 一、背景介绍 在现代互联网应用中,浏览器缓存机制被广泛采用以提升用户体验和减少网络流量消耗。然而,在某些情况下(如开发测试阶段或网站更新后),我们需要清除浏览器...
【禁止浏览器缓存当前文档内容】 在网页开发中,浏览器缓存机制是为了优化加载速度,但是有时会导致问题,比如服务器端更新的内容无法及时反映到客户端。为了防止这种情况,可以通过设置HTTP响应头来禁止浏览器缓存...
综上所述,浏览器缓存问题的解决需要综合考虑多个方面,包括但不限于HTTP缓存策略的合理设置、缓存污染的预防、个性化内容的安全处理、磁盘空间的有效管理和跨域资源共享的安全控制。通过上述措施的应用,可以有效...
Chrome浏览器缓存提取工具是一种专门用于从Google Chrome浏览器的本地存储缓存中检索和提取数据的实用程序。在日常互联网浏览中,Chrome会自动下载并存储网页的各种元素,如图片、视频、JavaScript文件等,以便在...
浏览器缓存问题的2种解决方案 在 Web 开发中,浏览器缓存是非常重要的一部分,它可以提高网站的加载速度和执行效率,但同时也带来了许多问题,例如当服务端修改了 js、css 等文件时,客户端无法更新,这就需要我们...
### 浏览器缓存资源隐藏 在日常的网络浏览过程中,我们的浏览器会自动缓存大量的网页资源,以便下次访问相同或类似页面时能够更快地加载。这些缓存通常保存在本地磁盘的一个特定文件夹内,对于Internet Explorer...
为了避免由于浏览器缓存导致的开发过程中出现的问题,开发人员需要确保每次修改文件后都能立即看到效果。此时就需要使用方法来清除浏览器的缓存。 ### 使用`jQuery.ajaxSetup({cache:false})`清除缓存 #### 代码...
ASP.NET MVC中使用jQuery时的浏览器缓存问题详解 ASP.NET MVC中使用jQuery时的浏览器缓存问题详解是指在ASP.NET MVC应用程序中使用jQuery库进行Ajax请求时遇到的浏览器缓存问题。浏览器缓存是指浏览器将服务器端...
在web开发领域,浏览器缓存和数据压缩是提高网站性能、优化用户体验的重要技术手段。它们可以帮助减少服务器的负载、加快页面加载速度、节省带宽资源,从而在竞争激烈的互联网市场中提供更加流畅、快捷的服务。 ###...
Chrome缓存资源导出工具是一款专门针对谷歌浏览器(Chrome)设计的应用程序,它允许用户方便地批量导出浏览器缓存中的各种资源,如图片和音频文件。这一工具对于开发者、网络分析师或者任何需要查看和备份浏览器缓存...