摘要
在一个有密码保护的Web应用当中,正确妥善的处理用户退出过程并不仅仅只需要调用HttpSession对象的invalidate()方法,因为现在大部分浏览器上都有后退(Back)和前进(Forward)按钮,允许用户后退或前进到一个页面。
在用户退出一个Web应用之后,如果按了后退按钮,浏览器把缓存中的页面呈现给用户,这会使用户产生疑惑,他们会开始担心他们的个人数据是否安全。
实际上,许多Web应用会弹出一个页面,警告用户退出时关闭整个浏览器,以此来阻止用户点击后退按钮。还有一些使用JavaScript,但在某些客户端浏览器中这却不一定起作用。这些解决方案大多数实现都很笨拙,且不能保证在任何情况下都100%有效,同时,它还要求用户有一定的操作经验。
这篇文章以简单的程序示例阐述了正确解决用户退出问题的方案。首先描述了一个理想的密码保护Web应用,然后以示例程序解释问题如何产生并讨论解决问题的方案。文章虽然是针对JSP进行讨论阐述,但作者所阐述的概念很容易理解而且能够为其他Web技术所采用。最后,用Jakarta Struts更为优雅地解决用户退出问题。文中包含JSP和Struts的示例程序。
大部分Web应用不会包含像银行账户或信用卡资料那样机密的信息,但是一旦涉及到敏感数据,就需要我们提供某些密码保护机制。例如,在一个工厂当中,工人必须通过Web应用程序访问他们的时间安排、进入他们的培训课程以及查看他们的薪金等等。此时应用SSL(Secure Socket Layer)就有些大材小用了(SSL页面不会在缓存中保存,关于SSL的讨论已经超出本文的范围)。但是这些应用又确实需要某种密码保护措施,否则,工人(在这种情况下,也就是Web应用的使用者)就可以发现工厂中所有员工的私人机密信息。
类似上面的情况还包括位于公共图书馆、医院、网吧等公共场所的计算机。在这些地方,许多用户共同使用几台计算机,此时保护用户的个人数据就显得至关重要。 同时应用程序的良好设计与实现对用户专业知识以及相关培训要求少之又少。
让我们来看一下现实世界中一个完美的Web应用是怎样工作的:
1、用户在浏览器中输入URL,访问一个页面。
2、Web应用显示一个登陆页面,要求用户输入有效的验证信息。
3、用户输入用户名和密码。
4、假设用户提供的验证信息是正确的,经过了验证过程,Web应用允许用户浏览他有权访问的区域。
5、退出时,用户点击页面的退出按钮,Web应用显示确认页面,询问用户是否真的需要退出。一旦用户点击确定按钮,Session结束,Web应用重新定位到登陆页面。用户现在可以放心的离开而不用担心他的信息会被泄露。
6、另一个用户坐到了同一台电脑前。他点击后退按钮,Web应用不应该显示上一个用户访问过的任何一个页面。
事实上,Web应用将一直停留在登陆页面上,除非第二个用户提供正确的验证信息,之后才可以访问他有权限的区域。
通过示例程序,文章向您阐述了如何在一个Web应用中实现上面的功能。
一、JSP samples
为了更为有效地向您说明这个解决方案,本文将从展示一个Web应用logoutSampleJSP1中碰到的问题开始。这个示例代表了许多没有正确解决退出过程的Web应用。logoutSampleJSP1包含一下JSP页面:login.jsp, home.jsp, secure1.jsp, secure2.jsp, logout.jsp, loginAction.jsp, 和 logoutAction.jsp。其中页面home.jsp, secure1.jsp, secure2.jsp, 和 logout.jsp是不允许未经认证的用户访问的,也就是说,这些页面包含了重要信息,在用户登陆之前或者退出之后都不应该显示在浏览器中。login.jsp页面包含了用于用户输入用户名和密码的form。logout.jsp页面包含了要求用户确认是否退出的form。loginAction.jsp和logoutAction.jsp作为控制器分别包含了登陆和退出动作的代码。
第二个Web示例应用logoutSampleJSP2展示了如何纠正示例logoutSampleJSP1中的问题。但是第二个示例logoutSampleJSP2自身也是有问题的。在特定情况下,退出问题依然存在。
第三个Web示例应用logoutSampleJSP3对logoutSampleJSP2进行了改进,比较妥善地解决了退出问题。
最后一个Web示例logoutSampleStruts展示了JakartaStruts如何优雅地解决退出问题。
二、Login action
解决方法便是引入用户自定义的安全实现机制,这就提供了更大的灵活性。
在用户自定义的认证方法中,普遍采用的方法是从用户提交的form中获得用户输入的认证信息,然后到诸如LDAP (lightweight directory access protocol)或关系数据库(relational database management system, RDBMS)的安全域中进行认证。如果用户提供的认证信息是有效的,登陆动作在HttpSession对象中保存某个对象。HttpSession存在着保存的对象则表示用户已经登陆到Web应用当中。为了方便起见,本文所附的示例只在HttpSession中保存一个用户名以表明用户已经登陆。清单1是从loginAction.jsp页面中节选的一段代码以此讲解登陆动作:
清单1
//... vATLinux联盟 //initialize RequestDispatcher object; set forward to home page vATLinux联盟 by default vATLinux联盟 RequestDispatcher rd = request.getRequestDispatcher( "home.jsp" ); //Prepare connection and statement vATLinux联盟 rs = stmt.executeQueryvATLinux联盟 ( "select password from USER where userName = '" + userName + "'" );vATLinux联盟 if (rs.next()) { vATLinux联盟 //Query only returns 1 record in the result set; vATLinux联盟 //Only 1 password per userName which is also the primary keyvATLinux联盟 if (rs.getString( "password" ).equals(password)) vATLinux联盟 { //If valid password vATLinux联盟 session.setAttribute( "User" , userName); //Saves username string in the session object vATLinux联盟 }vATLinux联盟 else { //Password does not match, i.e., invalid user password vATLinux联盟 request.setAttribute( "Error" , "Invalid password." ); rd = request.getRequestDispatcher( "login.jsp" );vATLinux联盟 }vATLinux联盟 } //No record in the result set, i.e., invalid username vATLinux联盟 else { request.setAttribute( "Error" , "Invalid user name." );vATLinux联盟 rd = request.getRequestDispatcher( "login.jsp" );vATLinux联盟 }vATLinux联盟 } //As a controller, loginAction.jsp finally either vATLinux联盟 forwards to "login.jsp" or "home.jsp" vATLinux联盟 rd.forward(request, response);vATLinux联盟 //...
|
本文当中所附Web应用示例均以关系型数据库作为安全域,但本问所讲述的内容同样适用于其他任何类型的安全域。 vATLinux联盟
三、Logout action
退出动作包含删除用户名以及调用用户的HttpSession对象的invalidate()方法。清单2是从loginoutAction.jsp中节选的一段代码,以此说明退出动作:
清单2
//... vATLinux联盟 session.removeAttribute( "User" );vATLinux联盟 session.invalidate();vATLinux联盟 //... |
四、阻止未经认证访问受保护的JSP页面
从提交的form中获取用户提交的认证信息并经过验证后,登陆动作仅仅在HttpSession对象中写入一个用户名。退出动作则刚好相反,它从HttpSession中删除用户名并调用HttpSession对象的invalidate()方法。为了使登陆和退出动作真正发挥作用,所有受保护的JSP页面必须首先验证HttpSession中包含的用户名,以便确认用户当前是否已经登陆。如果HttpSession中包含了用户名,就说明用户已经登陆,Web应用会将剩余的JSP页中的动态内容发送给浏览器。否则,JSP页将跳转到登陆页面,login.jsp。页面home.jsp, secure1.jsp, secure2.jsp和 logout.jsp均包含清单3中的代码段:
清单3
//... vATLinux联盟 String userName = (String) session.getAttribute( "User" );vATLinux联盟 if (null == userName) {vATLinux联盟 request.setAttribute( "Error" , "Session has ended. Please login." );vATLinux联盟 RequestDispatcher rd = request.getRequestDispatcher( "login.jsp" );vATLinux联盟 rd.forward(request, response);vATLinux联盟 }vATLinux联盟 //... vATLinux联盟 //Allow the rest of the dynamic content in this JSP to be served vATLinux联盟 to the browser vATLinux联盟 //...
|
在这个代码段中,程序从HttpSession中检索username字符串。如果username字符串为空,Web应用则自动中止执行当前页面并跳转到登陆页,同时给出错误信息“Session has ended. Please log in.”;如果不为空,Web应用继续执行,把剩余的页面提供给用户,从而使JSP页面的动态内容成为服务对象。
五、运行logoutSampleJSP1
运行logoutSampleJSP1将会出现如下几种情形:
◆如果用户没有登陆,Web应用将会正确中止受保护页面home.jsp, secure1.jsp, secure2.jsp和logout.jsp中动态内容的执行。也就是说,假如用户并没有登陆,但是在浏览器地址栏中直接敲入受保护JSP页的地址试图访问,Web应用将自动跳转到登陆页面,同时显示错误信息“Session has ended.Please log in.” vATLinux联盟
◆同样的,当一个用户已经退出,Web应用将会正确中止受保护页面home.jsp, secure1.jsp, secure2.jsp和logout.jsp中动态内容的执行。也就是说,用户退出以后,如果在浏览器地址栏中直接敲入受保护JSP页的地址试图访问,Web应用将自动跳转到登陆页面,同时显示错误信息“Session has ended.Please log in.” vATLinux联盟
◆用户退出以后,如果点击浏览器上的后退按钮返回到先前的页面,Web应用将不能正确保护受保护的JSP页面——在Session销毁后(用户退出)受保护的JSP页会重新显示在浏览器中。然而,点击该页面上的任何链接,Web应用都会跳转到登陆页面,同时显示错误信息“Session has ended.Please log in.”
六、阻止浏览器缓存
上述问题的根源就在于现代大部分浏览器都有一个后退按钮。当点击后退按钮时,默认情况下浏览器不会从Web服务器上重新获取页面,而是简单的从浏览器缓存中重新载入页面。这个问题并不仅限于基于Java(JSP/servlets/Struts) 的Web应用当中,在基于PHP (Hypertext Preprocessor)、ASP、(Active Server Pages)、和.NET的Web应用中也同样存在。
在用户点击后退按钮之后,浏览器到Web服务器(一般来说)或者应用服务器(在java的情况下)再从服务器到浏览器这样通常意义上的HTTP回路并没有建立。仅仅只是用户,浏览器和缓存之间进行了交互。所以即使受保护的JSP页面,例如home.jsp, secure1.jsp, secure2.jsp和logout.jsp包含了清单3上的代码,当点击后退按钮时,这些代码也永远不会执行的。
缓存的好坏,真是仁者见仁智者见智。缓存事实上的确提供了一些便利,但这些便利通常只存在于静态的HTML页面或基于图形或影像的页面。而另一方面,Web应用通常是面向数据的。由于Web应用中的数据频繁变更,所以与为了节省时间从缓存中读取并显示过期的数据相比,提供最新的数据显得尤为重要!
幸运的是,HTTP头信息“Expires”和“Cache-Control”为应用程序服务器提供了一个控制浏览器和代理服务器上缓存的机制。HTTP头信息Expires告诉代理服务器它的缓存页面何时将过期。HTTP1.1规范中新定义的头信息Cache-Control在Web应用当中可以通知浏览器不缓存任何页面。当点击后退按钮时,浏览器发送Http请求道应用服务器以便获取该页面的最新拷贝。如下是使用Cache-Control的基本方法:
◆no-cache:强制缓存从服务器上获取该页面的最新拷贝;vATLinux联盟
◆no-store:在任何情况下缓存不保存该页面。
HTTP1.0规范中的Pragma:no-cache等同于HTTP1.1规范中的Cache-Control:no-cache,同样可以包含在头信息中。
通过使用HTTP头信息的cache控制,第二个示例应用logoutSampleJSP2解决了logoutSampleJSP1的问题。logoutSampleJSP2与logoutSampleJSP1不同表现在如下代码段中,这一代码段加入进所有受保护的页面中:
清单4
//... vATLinux联盟 response.setHeader( "Cache-Control" , "no-cache" ); vATLinux联盟 //Forces caches to obtain a new copy of vATLinux联盟 the page from the origin server vATLinux联盟 response.setHeader( "Cache-Control" , "no-store" ); vATLinux联盟 //Directs caches not to store the page under any circumstance vATLinux联盟 response.setDateHeader( "Expires" , 0); vATLinux联盟 //Causes the proxy cache to see the page as "stale" vATLinux联盟 response.setHeader( "Pragma" , "no-cache" );vATLinux联盟 //HTTP 1.0 backward compatibility vATLinux联盟 String userName = (String) session.getAttribute( "User" );vATLinux联盟 if (null == userName) {vATLinux联盟 request.setAttribute( "Error" , "Session has ended. Please login." );vATLinux联盟 RequestDispatcher rd = request.getRequestDispatcher( "login.jsp" );vATLinux联盟 rd.forward(request, response);vATLinux联盟 }vATLinux联盟 //...
|
通过设置头信息和检查HttpSession对象中的用户名来确保浏览器不会缓存JSP页面。同时,如果用户未登陆,JSP页面的动态内容不会发送到浏览器,取而代之的将是登陆页面login.jsp。vATLinux联盟
Linux联盟收集整理 ,转贴请标明原始链接,如有任何疑问欢迎来本站Linux论坛讨论