一个健壮的Web应用程序在会话超时时不应该崩溃和死亡。我想我们都可以同意这一点,但鉴于HTTP的无状态特性以及通常将会话状态附加到该协议的漏洞,这相当困难。只需在Google上搜索一下 会话超时
。所以,我有了我的JSF/Seam/Ajax4JSF/jQuery应用程序,试图让它更加健壮。
会话超时发生在服务器上
嗯,当然了。HTTP会话以某种形式存储在服务器上。当然,你可能是一位认为RIA意味着客户端保留所有会话状态的激进分子,或者认为你不需要会话,因为你都是REST。好吧,把你的应用程序给我看看,我们再谈。你只有白皮书可以展示?太遗憾了。
如果你在服务器上保持会话状态,并且它自然会从服务器端清理并超时,那么客户端对这一点一无所知。因此,如果你想在会话超时时页面上用户正在查看的内容上执行任何操作,你需要轮询服务器。
在我的Seam应用程序中,我需要一些可以轮询的东西,所以我创建了一个在服务器上运行的以下组件
@Name("httpSessionChecker") @Scope(ScopeType.APPLICATION) public class HttpSessionChecker { @WebRemote public boolean isNewSession() { return ServletContexts.instance().getRequest().getSession().isNew(); } }
我知道这非常复杂。这里的关键是:当你问服务器会话是否超时时,你需要问它它是否有 当前 的会话是新的。你没有其他方法来找出(之前的)会话是否超时。因为你的轮询请求会在之前的会话超时后创建一个新的会话。所以如果你碰巧在之前的会话超时后检查isNewSession()你得到一个true的结果。(顺便问一下,谁有这个愚蠢的想法,认为Tomcat应该重新使用会话标识符?你能有多无能?)
轮询服务器
到那时,我仍然不知道当服务器上出现一个 新
的会话时,客户端想要做什么。好吧,首先我需要通过轮询服务器来找出这是否是情况
<script type="text/javascript" src="#{wikiPreferences.baseUrl}/seam/resource/remoting/resource/remote.js"></script> <script type="text/javascript" src="#{wikiPreferences.baseUrl}/seam/resource/remoting/interface.js?httpSessionChecker"></script> <script type="text/javascript"> var sessionChecker = Seam.Component.getInstance("httpSessionChecker"); var timeoutURL = '#{wiki:renderURL(wikiStart)}'; var timeoutMillis = '#{sessionTimeoutSeconds}'*1000+3000; var sessionTimeoutInterval = null; function startSessionTimeoutCheck() { sessionTimeoutInterval = setInterval('sessionChecker.isNewSession(alertTimeout)', timeoutMillis); } function stopSessionTimeoutCheck() { if (sessionTimeoutInterval) clearInterval(sessionTimeoutInterval); } function resetSessionTimeoutCheck() { stopSessionTimeoutCheck(); startSessionTimeoutCheck(); } function alertTimeout(newSession) { if (newSession) { clearInterval(sessionTimeoutInterval); jQuery(".ajaxSupport") .removeAttr('onblur') .removeAttr('onchange') .removeAttr('onkeyup') .removeAttr('onclick'); jQuery(".sessionEventTrigger").hide(); var answer = confirm("#{messages['lacewiki.msg.SessionTimeout']}"); if (answer) window.location = timeoutURL; } } </script>
首先,我导入Seam Remoting JavaScript接口以及服务器端组件的代理接口,httpSessionChecker。在客户端,我定义了两个主要函数:startSessionTimeoutCheck()和stopSessionTimeoutCheck()。启动函数开始以一定间隔轮询服务器。这个间隔不是一个随机的毫秒值。我每隔sessionTimeoutSeconds加上3秒(3000毫秒)轮询服务器。所以,如果服务器上配置的会话超时是30分钟,我每隔30分钟和3秒轮询一次,除非有人通过调用resetSessionTimeoutCheck()来重置间隔。这基本上保证,如果在浏览器窗口休眠期间没有发生任何请求,当再次轮询时,服务器端会话将消失。
这三个方法(启动、停止、重置)在将session超时检查与用户界面其余部分集成时将很有用。让我们看看我在重复服务器轮询的回调中做了什么,alertTimeOut(newSession):
- 如果服务器表示没有新的会话,我将继续以不变的时间间隔轮询它。
- 如果服务器表示有新的会话,我将通过清除间隔来停止轮询。然后我通过修改客户端的UI来对新会话做出反应。
从那时起,我处理客户端回调,可以对新会话做出任何反应。
适配客户端
我的目标是不仅在会话超时时将用户踢出。我不想仅仅将他重定向到某个起始页面,我想提供选择。但我也不想他进行任何可能导致会话消失后抛出异常的危险操作。
用户可以在确认对话框中点击确定
或取消
。如果点击确定
,我将重定向到起始页面,否则他可以留在当前页面。这将会成为一个问题,因为当前页面可能包含各种小部件,当你点击它们时,它们真的真的需要与某些数据的会话。
因此,首先,我使用jQuery禁用所有可能危险的操作。第一条jQuery语句从页面上具有CSS类ajaxSupport的所有元素中删除JavaScript事件。第二条语句隐藏页面上具有CSS类sessionEventTrigger的所有元素。具有类ajaxSupport的元素通常是具有onblur事件触发的输入字段,或者onclick链接。
用户可以继续将这些输入字段中的内容复制出来(以保留否则会丢失的数据),但他不能再触发AJAX请求。他也不能点击任何按钮或看到可能需要会话的任何元素。
所以这一切都归结为以下两个问题
- 哪些页面需要会话超时检查?有时我对服务器端会话并不关心,因为给定页面上的所有操作可能都是安全的,无论它们是在之前的HTTP会话还是新的HTTP会话中运行。
- 在页面上的哪些元素我想隐藏,以及在服务器端会话超时(如果用户不接受重定向到起始页面)时我想禁用哪些操作?
如果一个页面需要会话超时检查,我将在其body中添加以下代码
<script type="text/javascript">startSessionTimeoutCheck();</script>
页面加载时,我以给定的时间间隔开始轮询服务器。如果服务器上有新的会话,所有标记为styleClass="ajaxSupport"的元素将使用jQuery剥离其事件处理器。这通常是在页面表单上的某些输入字段
<s:decorate id="userNameDecorate" template="formFieldDecorate.xhtml"> <ui:define name="label">#{messages['lacewiki.label.commentForm.Name']}</ui:define> <h:inputText styleClass="ajaxSupport" tabindex="1" size="40" maxlength="100" required="true" id="userName" value="#{commentHome.instance.fromUserName}"> <a:support status="commentForm:status" event="onblur" reRender="userNameDecorate" oncomplete="onAjaxRequestComplete()"/> </h:inputText> </s:decorate>
如果服务器上存在新的会话,将出现确认对话框。如果用户点击取消
,则onblur该输入字段的将禁用事件。用户仍然可以复制并恢复他输入的值。
所有具有sessionEventTrigger类的元素将在此时隐藏。这些通常是如保存
、更新
等按钮。这里有一个例子
<a:commandLink id="post" action="#{commentHome.persist}" tabindex="1" reRender="commentDisplayForm, messageBoxContainer" accesskey="#{messages['lacewiki.button.commentForm.Post.accesskey']}" status="commentForm:status" eventsQueue="ajaxEventQueue" oncomplete="onAjaxRequestComplete()" styleClass="button sessionEventTrigger"> <h:outputText escape="false" styleClass="buttonLabel" value="#{messages['lacewiki.button.commentForm.Post']}"/> </a:commandLink>
只是一个巧合,这是一个不会触发页面导航但在AJAX请求完成后进行部分页面渲染的按钮。现在,这是我们需要考虑的额外复杂性。
处理AJAX请求
如果你请求一个页面,当页面渲染时会话轮询间隔开始运行,那么一切正常。轮询间隔将是/session timeout/ + 3秒(这只是安全边际)。所以,当你在该页面上专注并工作的时候,没有发送请求到服务器,客户端和服务器都保持计算秒数。服务器在/session timeout/后超时,3秒后客户端询问服务器是否有新的会话。服务器应该回答/是/。
AJAX完全改变了这一局面。包含AJAX操作的页面按定义支持该页面的部分重新渲染。所以,当你的页面会话轮询间隔在客户端开始运行时,你向服务器发送AJAX请求(触发onblur事件、点击AJAX按钮等)。现在,客户端轮询间隔和服务器会话超时不同步。因此,每次AJAX请求完成后,你必须重置客户端轮询间隔。这正是上面代码片段中看到的oncomplete="onAjaxRequestComplete()"回调的作用。
以下是这个JavaScript函数的作用
function onAjaxRequestComplete() { resetSessionTimeoutCheck(); }
我想你可以直接调用重置,但我喜欢间接调用。有时我需要在AJAX请求完成后做其他事情(比如,将CSS样式应用到页面重新渲染的部分)。我实际上非常希望能够为所有Ajax4JSF事件提供一个默认的回调。
最后,我可以有条件地使用会话检查轮询的启动/停止功能。比如说,我有一个不需要任何会话检查的页面,所有操作和链接在使用旧会话或新会话时都是完全安全的。
除了这个页面可能也会条件性地包含一个带有大量AJAX魔法的表单。所以我需要在表单包含并显示时启用会话状态轮询,并在表单不显示时禁用它。基本上,我需要在同一页面上有条件地开始和停止轮询。
<s:fragment rendered="#{commentHome.showForm}"> <script type="text/javascript">startSessionTimeoutCheck();</script> </s:fragment>
相反的,这可能是页面首次加载时的默认条件
<s:fragment rendered="#{not commentHome.showForm}"> <script type="text/javascript">stopSessionTimeoutCheck();</script> </s:fragment>
停止轮询是可以的,即使它没有开始,JS函数是安全的。
嗯,这就是全部内容,我希望我们可以将其部分回滚到Seam和Ajax4JSF代码库,使其更容易使用。