在一个有密码保护的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);
//...
listing 2
//...
session.removeattribute("user");
session.invalidate();
//...
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
//...
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);
}
//...
清单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);
//...
清单6
public abstract class baseaction extends action {
public actionforward execute(actionmapping mapping, actionform form,httpservletrequest request, httpservletresp
onse 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;
}
}
清单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应用中混合使用这些方案能够使拥护的私人数据不致泄露,同时,也能增加用户的经验。
本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/lip8654/archive/2008/02/26/2121283.aspx