在一个有密码保护的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示例
为了更为有效地阐述实现方案,本文将从展示一个示例应用logoutSample
JSP1中碰到的问题开始。这个示例代表了许多没有正确解决退出过程的Web应用。logoutSample
JSP1包含了下述
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作为控制器分别包含了登陆和退出代码。
第二个示例应用logoutSample
JSP2展示了如何解决示例logoutSample
JSP1中的问题。然而,第二个应用自身也是有疑问的。在特定的情况下,退出问题还是会出现。
第三个示例应用logoutSample
JSP3在第二个示例上进行了改进,比较完善地解决了退出问题。
最后一个示例logoutSample
Struts展示了
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应用则继续执行,也就是把剩余的页面提供给
用户。
运行logoutSample
JSP1
运行logoutSample
JSP1将会出现如下几种情形:
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回路并没有建立,仅仅只是
用户,浏览器和缓存进行了交