65.9K
CodeProject 正在变化。 阅读更多。
Home

简单、跨浏览器 JavaScript 会话和表单身份验证超时预防/处理

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.47/5 (9投票s)

2009年12月15日

CPOL

8分钟阅读

viewsIcon

54272

一种客户端解决方案,用于警告用户他们的会话即将超时,并在必要时刷新他们的身份验证票证。

引言

本文创建了一个集中的解决方案,该方案会警告用户他们的登录会话即将超时,并让他们选择在不回发当前页面或离开页面的情况下“刷新”他们的表单身份验证票证。由于它基于 JavaScript 并且不直接修改表单身份验证票证,因此该解决方案快速且安全,并且在不必要时不会回发到服务器。

背景

我一直在寻找一种有效、简单且跨浏览器友好的方法,可以优雅地处理 ASP.NET 2.0 应用程序的会话超时和相关的表单身份验证登出事件。我意识到在将 UpdatePanel 包装在 GridView(目的是让分页和排序对用户来说显得“更流畅”)之后,当在会话已超时的情况下向服务器发送了部分页面回发时,它会非常不优雅地失败。发生这种情况时,用户会收到来自 UpdatePanel 的一个丑陋、充斥术语的弹出消息,该消息既没有解释发生了什么,也没有告诉用户接下来该做什么(除非他们理解 AJAX!)。

我决定要么编写所有 UpdatePanel 来处理会话超时,要么创建一个集中的解决方案来优雅地处理每个表单身份验证保护页面上的所有会话超时。为此,我需要一个能够做到三件事的解决方案:

  1. 在 x 分钟不活动后,警告用户他们的会话即将过期,并在会话实际超时之前将他们登出,
  2. 为用户提供一个可点击的按钮,该按钮可在不回发页面或导航到另一页的情况下续订他们的会话票证,并且
  3. 主要在客户端工作,以防止不必要的服务器回发。

我在网上找到了几个解决方案,但没有一个满足我的所有需求,所以我借鉴了其他解决方案的最佳方面,并加入了我自己的调整,使其更有用。当然,我的解决方案不能损害表单身份验证服务或身份验证票证等的安全性,因此我通过不直接接触实际的身份验证票证/Cookie 来避免了这种可能性,并且仅在会话过期时/如果过期调用 FormsAuthentication.SignOut() 来登出用户。我找到的下一个最好的解决方案依赖于 AJAX 来不断刷新会话,但该方法会产生不必要的服务器回调,而我不想在我的应用程序中使用它。

Using the Code

首先,JavaScript。将此脚本标签放在主页(Master page)的 Head 中,或者放在您的安全页面的基类(base-class)的 head 中。也可以使用代码隐藏(code-behind)中的 StringBuilder 构建该标签,并使用 ScriptManager 及其脚本注册方法动态添加到页面;只需确保将其添加到 Head 中。如果您没有使用单一的主页或页面基类来派生所有表单身份验证的 ASPX 页面,我强烈推荐这样做。将所有页面的内容和布局基于一个集中的页面,并将更改级联到其所有派生页面,这实在是太方便了。如果您不使用主页,请将此脚本标签放在每个表单身份验证保护的 ASPX 页面的 Head 中。

<script type="text/javascript" language="javascript">
// set your timeout period in minutes, minus 1;
// ie: 9 minutes + 1 minute countdown = 10 minute timeout
var timeoutPeriod = 9;
// declare global variables to track the timeout
// and interval timers;
// this allows Javascript to cancel them as necessary
var intervalCountdown;
var timeoutWarning;

function showWarning(){
  // show the warning divs
  document.getElementById('divTimeOut').style.display = 'block';
  intervalCountdown = setInterval(countDown,1000);
  // start the countdown
}
function countDown(){
  var divCountDown = document.getElementById('divCountDown');
  var intCount = parseInt(divCountDown.innerHTML) -1;
  // get remaining countdown time, minus 1 second

  if (intCount <= 10)
    {divCountDown.style.color = '#ff0000';}
     // show red text when only 10 seconds remain
  if (intCount >= 0)
    {divCountDown.innerHTML = intCount.toString();}
    // update the countdown div
  else // logout user after 10 minutes
    {__doPostBack('ctl00$lnkTimeOut','');}
    // if your Master page is named something other than
    // "ctl00," change that here; you can also
    // call the "click" event of the button instead
}
function refreshPage(){
clearInterval(intervalCountdown); // stop countdown
clearTimeout(timeoutWarning);
// reset timeout warning to original 9 minutes
timeoutWarning = setTimeout(showWarning,timeoutPeriod * 60000);
var divCountDown = document.getElementById('divCountDown');
divCountDown.style.color = '#000000';
divCountDown.innerHTML = '60'; // reset countdown to 60
// create random number between 0 and 1000
var decRound = Math.round(Math.random()*1000);
var imgRefresh = new Image(1,1);
// change '../keepalive.aspx?id=' based on keepalive.aspx's location
// in relation to your secured pages, ie '/keepalive.aspx?id=' + 
//  decRound.toString(); for a keepalive.aspx page placed in the application root
imgRefresh.src = '../keepalive.aspx?id=' + decRound.toString();
document.getElementById('divTimeOut').style.display = 'none';
// hide the warning divs
}
</script>

JavaScript 函数执行以下操作:showWarning() 在屏幕上显示警告 div,并启动一个 60 秒的“倒计时”。countDown() 每秒触发一次,并将计数减 1。在零时,countDown() 会在“登出” LinkButton 上触发一个回发。refreshPage() 在用户点击“保持登录”按钮时关闭倒计时,并使用随机生成的查询字符串从服务器请求“keepalive.apsx”页面。(上面的 JavaScript 也可以放在您链接到主页 Head 的 .js 文件中。)

接下来,在主页的 body onload 事件中设置 JavaScript 超时,以便在 9 分钟后触发“showWarning”函数。

<body onload="timeoutWarning = setTimeout(showWarning, timeoutPeriod * 60000);">
<form id="form1" runat="server"><div>aspcontent ...blah blah blah etc...</div>
</form>
</body>

如果没有主页,请将上述 onload 事件代码放在您使用表单身份验证保护的页面的 body 标签中,或使用 Page.RegisterStartupScript 动态添加。在 9 分钟不活动(没有完整的回发或页面重定向)后,超时警告 div 将出现,并开始 60 秒的倒计时,之后用户将被登出。这本质上创建了一个 10 分钟(客户端)的会话超时(您可以更改 timeoutPeriod 全局变量的值以适应您的需求,但请务必相应地调整您的 web.config,如下所述)。如果登出警告计时器达到 0,JavaScript 将调用 __doPostBack('ctl00$lnkTimeOut','');,这将触发与 asp:LinkButtonlnkTimeOut”关联的代码隐藏。此按钮可以是您的常规登出按钮,也可以是一个带有“display:none;” CSS 样式设置的“隐藏”按钮。稍后会详细介绍...

现在,负责“刷新”会话和表单身份验证票证 Cookie 的 ASPX 页面……“keepalive.aspx”。

<%@ Page Language="vb" AutoEventWireup="false" %>
<%@ OutputCache Location="None" VaryByParam="None" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
    <title></title>
</head>
<body>
</body>
</html>

keepalive.aspx”就到此为止。没有代码隐藏。无需其他任何东西。页面已添加 OutputCache Location="None"VaryByParam="None" 选项,以尝试阻止浏览器在客户端缓存页面,这将使该页面无法每次请求时都刷新会话。我发现此解决方案在不同浏览器上的效果有些不稳定,因此 JavaScript refreshPage() 函数使用 Math.random() 数字函数创建一个唯一的查询字符串,并在每次向 keepalive.aspx 发出请求时将其附加到 URL。这可以防止浏览器认为我们再次调用同一个 keepalive.aspx 页面,从而“帮助”我们从缓存中获取它,而不是直接从服务器获取。对 keepalive.aspx 的 JavaScript 请求实际上看起来像 keepalive.aspx?id=79keepalive.aspx?id=420 等,强制浏览器将请求全部发送到服务器。我想,在同一页面对 keepalive.aspx 进行约 1000 次请求后,此方法将失败,但谁会在每次刷新之间等待 9 或 10 分钟,然后刷新 1000 次“会话”呢???

对于实际的弹出式 div 来警告用户,请将其放在主页的 body 中。

<div id="divTimeOut" class="divTOwarning">
   <div id="divTimeOutI" class="divTOwarningI">
   Your login session is about to expire... Click below if you want to stay logged in.
<input type="button" id="btnRefresh" onclick="refreshPage();" 
  value="Stay Logged In" style="margin:4px 0 4px 0;" />
<div style="height:18px;">Logging out in:<div 
  id="divCountDown" style="font-size:20px;">60</div></div>
</div></div>
<asp:LinkButton ID="lnkTimeOut" runat="server" 
  style="display:none;"></asp:LinkButton>

此代码片段还显示了隐藏的登出 LinkButtonlnkTimeOut”。我使用了一个“隐藏”的登出按钮(不同于我的常规登出按钮),以便当用户因会话超时而被登出时,“login.aspx”页面会显示一个 div,上面写着:“您因 10 分钟不活动而被登出。请重新登录以继续。”。这个简单的消息在 ASP.NET 的表单身份验证超时功能的实现中是严重缺失的。就我个人而言,我知道当一个网站因为不活动而将我登出时,我希望知道发生了什么,而不是仅仅看到一个空白的登录页面!lnkTimeOut 的代码隐藏

Private Sub lnkTimeOut_Click(ByVal sender As Object, _
        ByVal e As System.EventArgs) Handles lnkTimeOut.Click
    Session.Clear()
    Session.Abandon()
    FormsAuthentication.SignOut()
    Dim strUrl As String = Server.UrlEncode(Request.RawUrl)
    Response.Redirect("../../login.aspx?id=1&ReturnUrl=" & strUrl)
    'Change the location and name of your login page here
End Sub

id=1”查询字符串告诉我的 login.aspx 页面显示一个通常隐藏的 div,让用户知道发生了什么。第二个查询字符串“ReturnUrl”告诉 login.aspx 页面在用户重新登录后,将他们重定向回他们被登出时正在查看的页面。

相关的 CSS 在这里。

.divTOwarning {width:100%;height:100%;z-index:140;
   background-image:url(../images/bkTimeout.png);display:none;
   background-repeat:repeat-x;background-position:left top;
   background-attachment:fixed;position:fixed;top:0;left:0;}

.divTOwarningI {width:200px;height:130px;position:fixed;top:40%;
   left:35%;border:solid 1px #000000;
   z-index:150;background-color:#9EFA82;font-size:14px;
   text-align:center;padding:3px 3px 3px 3px;}

此 CSS 创建了一个小的“内部” div,其中包含刷新按钮、警告消息和倒计时,这些都居中在一个全屏的 div 中,该 div 会“灰色化”页面的其余部分。我用作背景的图像 bkTimeout.png 是一个 2 像素宽 x 800 像素高的 PNG 图像,它是深灰色且具有 50% 的透明度。此 PNG 会填满页面(使用 repeat-x),并给用户一种感觉,即页面的其余部分已被“锁定”,直到他们选择通过点击按钮来“刷新”登录会话。设置 z-index 确保警告 divs 出现在页面其他内容之上,而设置 display:none; 则会隐藏这些 div,直到它们被 showWarning()“显示”为止。

***关于使用 style="display:none;" vs visible="false" 的说明... 在 ASPX 页面或代码隐藏中将元素设置为 visible="false" 会告诉 ASP.NET 渲染该元素,这意味着 JavaScript 无法与之交互。在 CSS 中将 display:none; 设置为 display:none; 仍然会渲染对象,但只是使其对用户不可见。这样,JavaScript 就可以在客户端访问不可见的元素并根据需要“显示”它。***

最后,我们希望发生标准的 ASP.NET 表单身份验证超时(再次强调,内置超时事件根本优雅),因此我们将 web.config 文件中的超时间隔设置为比实际的 10 分钟超时多一分钟。这保证了用户永远不会在没有警告的情况下被 ASP.NET 应用程序登出。

<authentication mode="Forms">
    <forms name=".ASPXFORMSAUTH"
           loginUrl="login.aspx"
           protection="All"
           timeout="11"
           path="/"
           requireSSL="false"
           slidingExpiration="true"
           defaultUrl="default.aspx"
           cookieless="UseDeviceProfile"
           enableCrossAppRedirects="false"/>
</authentication>

一些编码人员注意到,在 web.config 文件中设置 slidingExpiration="true" 意味着只有在其过期时间过去 50% 以上时,身份验证 Cookie 会被刷新。我不知道这是否属实,但这不应该影响此解决方案,除非您将超时值设置得非常短,例如 1 到 4 分钟。

这就是让您的用户不因会话超时而被表单身份验证保护的 ASP.NET 页面粗暴登出的所有内容。请注意,如果您使用 UpdatePanel 来进行部分页面回发,您将需要在这些页面上实现 PageRequestManager 来重置 JavaScript 超时间隔为九分钟,并在每次异步回发时“刷新”身份验证票证。在包含 UpdatePanel 的页面 body 中,应该有一个类似这样的标签:

<script type="text/javascript" language="javascript">
  var prm = Sys.WebForms.PageRequestManager.getInstance();
  prm.add_pageLoaded(refreshPage);
</script>

此外,如果页面中有任何按钮或链接需要刷新超时周期(如果这些控件不执行完整回发或导航到新页面),您可以通过将 OnClientClick="refreshPage();" 添加到 ASP.NET 控件,或将 onclick="refreshPage();" 添加到由服务器处理的元素(如 anchor 或 input type="button" 控件)来使这些链接或按钮重置 JavaScript“警告”计时器并刷新身份验证票证。

此 ASP.NET 2.0 代码和相关 JavaScript 已在 IE 7、IE 8、Firefox 3.5、Opera 10、Safari 4(Windows 版)和 Chrome 3 中进行过测试。

祝您编码愉快!

© . All rights reserved.