16

我们有一个使用本机表单身份验证和会话功能的 ASP.NET 4.5 WebForms 应用程序。两者都有 20 分钟的超时时间和滑动到期。

想象以下场景。用户在我们的应用程序中工作了一段时间,然后继续做一些其他事情,让我们的应用程序空闲 20 分钟。然后用户返回到我们的应用程序以编写报告。但是,当用户尝试保存时,他/她会被视为登录屏幕,并且报告会丢失。

显然,这是不希望的。我们希望在身份验证或会话到期时将浏览器重定向到登录页面,而不是这种情况。为了实现这一点,我们构建了一个 Web Api 服务,可以调用它来检查是否是这种情况。

public class SessionIsActiveController : ApiController
{
    /// <summary>
    /// Gets a value defining whether the session that belongs with the current HTTP request is still active or not.
    /// </summary>
    /// <returns>True if the session, that belongs with the current HTTP request, is still active; false, otherwise./returns>
    public bool GetSessionIsActive()
    {
        CookieHeaderValue cookies = Request.Headers.GetCookies().FirstOrDefault();
        if (cookies != null && cookies["authTicket"] != null && !string.IsNullOrEmpty(cookies["authTicket"].Value) && cookies["sessionId"] != null && !string.IsNullOrEmpty(cookies["sessionId"].Value))
        {
            var authenticationTicket = FormsAuthentication.Decrypt(cookies["authTicket"].Value);
            if (authenticationTicket.Expired) return false;
            using (var asdc = new ASPStateDataContext()) // LINQ2SQL connection to the database where our session objects are stored
            {
                var expirationDate = SessionManager.FetchSessionExpirationDate(cookies["sessionId"].Value + ApplicationIdInHex, asdc);
                if (expirationDate == null || DateTime.Now.ToUniversalTime() > expirationDate.Value) return false;
            }
            return true;
        }
        return false;
    }
}

客户端每 10 秒调用一次此 Web Api 服务,以检查身份验证或会话是否已过期。如果是这样,脚本会将浏览器重定向到登录页面。这就像一个魅力。

但是,调用此服务会触发身份验证和会话的滑动到期。因此,本质上,创建永无止境的身份验证和会话。我在服务开始时设置了一个断点,以检查它是否是我们自己的函数之一触发它。但事实并非如此,它似乎发生在 ASP.NET 更深处的某个地方,在服务执行之前。

  1. 有没有办法为特定请求禁用触发 ASP.NET 的身份验证和会话滑动到期?
  2. 如果没有,解决这种情况的最佳做法是什么?
4

3 回答 3

13
  1. 这似乎是不可能的。一旦启用了滑动到期,它总是会被触发。如果有一种方法可以在不扩展会话的情况下访问它,那么我们一直无法找到它。

  2. 那么如何应对这种情况呢?对于问题中最初提出的解决方案,我们提出了以下替代解决方案。这个实际上更有效,因为它不使用网络服务每隔 x 秒给家里打电话。

因此,我们希望有一种方法可以知道 ASP.NET 的表单身份验证或会话何时过期,以便我们可以主动注销用户。每个页面上的简单 javascript 计时器(由 Khalid Abuhakmeh提出)是不够的,因为用户可以同时在多个浏览器窗口/选项卡中使用应用程序。

为了使这个问题更简单,我们做出的第一个决定是使会话的过期时间比表单身份验证的过期时间长几分钟。这样,会话将永远不会在表单身份验证之前过期。如果下次用户尝试登录时有一个挥之不去的旧会话,我们会放弃它以强制一个新的会话。

好的,所以现在我们只需要考虑表单身份验证过期。

接下来,我们决定禁用表单身份验证的自动滑动到期(在 web.config 中设置)并创建我们自己的版本。

public static void RenewAuthenticationTicket(HttpContext currentContext)
{
    var authenticationTicketCookie = currentContext.Request.Cookies["AuthTicketNameHere"];
    var oldAuthTicket = FormsAuthentication.Decrypt(authenticationTicketCookie.Value);
    var newAuthTicket = oldAuthTicket;
    newAuthTicket = FormsAuthentication.RenewTicketIfOld(oldAuthTicket); //This triggers the regular sliding expiration functionality.
    if (newAuthTicket != oldAuthTicket)
    {
        //Add the renewed authentication ticket cookie to the response.
        authenticationTicketCookie.Value = FormsAuthentication.Encrypt(newAuthTicket);
        authenticationTicketCookie.Domain = FormsAuthentication.CookieDomain;
        authenticationTicketCookie.Path = FormsAuthentication.FormsCookiePath;
        authenticationTicketCookie.HttpOnly = true;
        authenticationTicketCookie.Secure = FormsAuthentication.RequireSSL;
        currentContext.Response.Cookies.Add(authenticationTicketCookie);
        //Here we have the opportunity to do some extra stuff.
        SetAuthenticationExpirationTicket(currentContext);
    }
}

OnPreRenderComplete我们从应用程序的 BasePage 类中的事件调用此方法,所有其他页面都从该类继承。它与正常的滑动到期功能完全相同,但我们有机会做一些额外的事情;比如调用我们的SetAuthenticationExpirationTicket方法。

public static void SetAuthenticationExpirationTicket(HttpContext currentContext)
{
    //Take the current time, in UTC, and add the forms authentication timeout (plus one second for some elbow room ;-)
    var expirationDateTimeInUtc = DateTime.UtcNow.AddMinutes(FormsAuthentication.Timeout.TotalMinutes).AddSeconds(1);
    var authenticationExpirationTicketCookie = new HttpCookie("AuthenticationExpirationTicket");
    //The value of the cookie will be the expiration date formatted as milliseconds since 01.01.1970.
    authenticationExpirationTicketCookie.Value = expirationDateTimeInUtc.Subtract(new DateTime(1970, 1, 1)).TotalMilliseconds.ToString("F0");
    authenticationExpirationTicketCookie.HttpOnly = false; //This is important, otherwise we cannot retrieve this cookie in javascript.
    authenticationExpirationTicketCookie.Secure = FormsAuthentication.RequireSSL;
    currentContext.Response.Cookies.Add(authenticationExpirationTicketCookie);
}

现在我们有一个额外的 cookie 可供我们使用,它始终代表正确的表单身份验证过期时间,即使用户在不同的浏览器窗口/选项卡中工作。毕竟,cookie 的范围很广。现在唯一剩下的就是一个 javascript 函数来验证 cookie 的值。

function CheckAuthenticationExpiration() {
    var c = $.cookie("AuthenticationExpirationTicket");
    if (c != null && c != "" && !isNaN(c)) {
        var now = new Date();
        var ms = parseInt(c, 10);
        var expiration = new Date().setTime(ms);
        if (now > expiration) location.reload(true);
    }
}

(请注意,我们使用jQuery Cookie 插件来检索 cookie。)

将此功能放在一个间隔中,用户将在他或她的表单身份验证过期的那一刻被注销。Voilà :-) 这个实现的一个额外好处是您现在可以控制表单身份验证的到期时间延长。如果您想要一堆不延长过期时间的 Web 服务,请不要RenewAuthenticationTicket为它们调用方法。

如果您有什么要补充的,请发表评论!

于 2013-04-04T16:08:07.583 回答
1

您的网站功能应该可以在没有 JavaScript 的情况下运行,或者您只需将一个问题替换为另一个问题。我也解决了这个问题,这是如何解决的:

当您对自己进行身份验证时,会创建会话 cookie,默认生命周期为 20 分钟。当此到期时,用户将被注销。

饼干图像

当用户在登录表单中选择“记住我”时,会在客户端和数据库中创建额外的持久性 cookie [AuthCookie]。此 cookie 的有效期为 1 个月。每当加载页面时,会话和持久性 cookie 数据都会以新的生命周期重新创建(通常您想要解密/加密票证)。

想象以下场景。用户在我们的应用程序中工作了一段时间,然后继续做一些其他事情,让我们的应用程序空闲 20 分钟。然后用户返回到我们的应用程序以编写报告。当用户尝试保存时,他的会话会在请求之前恢复。

一种方法是扩展 global.aspx 以处理预请求。类似的东西:

    void application_PreRequestHandlerExecute(object sender, EventArgs e){
        ...
        if (HttpContext.Current.Handler is IRequiresSessionState) {
            if (!context.User.Identity.IsAuthenticated)
                AuthService.DefaultProvider.AuthenticateUserFromExternalSource();

AuthenticateUserFromExternalSource 应该检查 cookie 数据是否与数据库匹配,因为存储在客户端的任何内容都可以更改。如果您拥有具有访问权限的付费服务,那么您需要检查用户是否仍然拥有这些权限,然后您可以重新创建会话。

于 2014-03-14T11:18:40.153 回答
0

这一切都可以在客户端解决,无需返回服务器。

在 JavaScript 中这样做。

 var timeout = setTimeout(function () {
      window.location = "/login";
 }, twentyMinutesInMilliseconds + 1);

每次页面刷新时,超时将设置为 20 分钟。这确保了用户需要在超时发生之前完成所有工作。很多站点都使用这种方法,它可以让您免于执行不必要的服务器请求。

于 2013-03-27T17:15:23.940 回答