一个健壮的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代码库,使其更容易使用。


返回顶部