简体   繁体   English

关于ASP.NET表单身份验证和会话的滑动到期

[英]Concerning the sliding expiration of ASP.NET's forms authentication and session

We have a ASP.NET 4.5 WebForms application using the native forms authentication and session functionality. 我们有一个使用本机表单身份验证和会话功能的ASP.NET 4.5 WebForms应用程序。 Both have a timeout of 20 minutes with sliding expiration. 滑动到期时两者都超时20分钟。

Imagine the following scenario. 想象一下以下场景。 A user has worked in our application for a while and then proceeds to do some other things, leaving our application idle for 20 minutes. 用户已经在我们的应用程序中工作了一段时间,然后继续做其他事情,让我们的应用程序闲置20分钟。 The user then returns to our application to write a report. 然后,用户返回到我们的应用程序以编写报告。 However, when the user tries to save, he/she is treated with the login screen, and the report is lost. 但是,当用户尝试保存时,他/她将使用登录屏幕进行处理,并且报告将丢失。

Obviously, this is unwanted. 显然,这是不需要的。 Instead of this scenario, we want the browser to be redirected to the login page the moment either authentication or session has expired. 我们希望在身份验证或会话过期时将浏览器重定向到登录页面,而不是这种情况。 To realize this, we have build a Web Api service that can be called to check whether this is the case. 为了实现这一点,我们构建了一个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;
    }
}

This Web Api service is called every 10 seconds by the client to check if either authentication or session has expired. 客户端每10秒调用此Web Api服务,以检查身份验证或会话是否已过期。 If so, the script redirects the browser to the login page. 如果是,则脚本将浏览器重定向到登录页面。 This works like a charm. 这就像一个魅力。

However, calling this service triggers the sliding expiration of both authentication and session. 但是,调用此服务会触发身份验证和会话的滑动到期。 Thus, essentially, creating never ending authentication and session. 因此,基本上,创建永无止境的身份验证和会话。 I have set a breakpoint at the start of the service to check if it is one of our own functions that triggers this. 我在服务的开头设置了一个断点,以检查它是否是我们自己的一个触发它的函数。 But this is not the case, it seems to occur somewhere deeper in ASP.NET, before the execution of the service. 但事实并非如此,它似乎发生在ASP.NET的更深处,在服务执行之前。

  1. Is there a way to disable the triggering of ASP.NET's authentication and session sliding expirations for a specific request? 有没有办法禁用ASP.NET的身份验证和会话滑动到期特定请求的触发?
  2. If not, what is best practice to tackle a scenario like this? 如果没有,那么解决这样的场景的最佳做法是什么?
  1. This seems to be impossible. 这似乎是不可能的。 Once sliding expiration is enabled, it is always triggered. 启用滑动到期后,始终会触发它。 If there is a way to access the session without extending it, we have not been able to find it. 如果有一种方法可以访问会话而不扩展它,我们无法找到它。

  2. So how to tackle this scenario? 那么如何解决这个问题呢? We came up with the following alternative solution to the one originally proposed in the question. 我们提出了以下替代解决方案,以解决最初在问题中提出的解决方案。 This one is actually more efficient because it doesn't use a web service to phone home every x seconds. 这个实际上更有效率,因为它不会每隔x秒使用Web服务回家。

So we want to have a way to know when either ASP.NET's forms authentication or session has expired, so we can pro-actively logout the user. 因此,我们希望有一种方法可以知道ASP.NET的表单身份验证或会话何时过期,因此我们可以主动注销用户。 A simple javascript timer on every page ( as proposed by Khalid Abuhakmeh) would not suffice because the user could be working with the application in multiple browser windows/tabs at the same time. 每个页面上的一个简单的javascript计时器(由Khalid Abuhakmeh 提出 )是不够的,因为用户可能同时在多个浏览器窗口/标签中使用该应用程序。

The first decision we made to make this problem simpler is to make the expiration time of the session a few minutes longer than the expiration time of the forms authentication. 我们为使此问题更简单而做出的第一个决定是使会话的到期时间比表单身份验证的到期时间长几分钟。 This way, the session will never expire before the forms authentication. 这样,会话将永远在表单身份验证之前到期。 If there is a lingering old session the next time the user tries to log in, we abandon it to force a fresh new one. 如果下次用户尝试登录时存在挥之不去的旧会话,我们会放弃它以强制刷新新会话。

All right, so now we only have to take the forms authentication expiration into account. 好的,现在我们只需考虑表单身份验证到期。

Next, we decided to disable the forms authentication's automatic sliding expiration (as set in the web.config) and create our own version of it. 接下来,我们决定禁用表单身份验证的自动滑动过期(在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);
    }
}

We call this method from the OnPreRenderComplete event in our application's BasePage class, from which every other page inherits. 我们从应用程序的BasePage类中的OnPreRenderComplete事件中调用此方法,每个其他页面都从该类继承。 It does exactly the same thing as the normal sliding expiration functionality, but we get the opportunity to do some extra stuff; 它与正常的滑动过期功能完全相同,但我们有机会做一些额外的事情; like call our SetAuthenticationExpirationTicket method. 比如调用我们的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);
}

Now we have an extra cookie at our disposal that always represents the correct forms authentication expiration time, even if the user works in different browser windows/tabs. 现在我们有一个额外的cookie,即使用户在不同的浏览器窗口/标签中工作,也始终代表正确的表单身份验证到期时间。 After all, cookies have a browser wide scope. 毕竟,cookie具有浏览器范围。 Now the only thing left is a javascript function to verify the cookie's value. 现在唯一剩下的是一个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);
    }
}

(Note that we use jQuery Cookie Plugin to retrieve the cookie.) (请注意,我们使用jQuery Cookie插件来检索cookie。)

Put this function in an interval, and users will be logged out the moment his or her forms authentication has expired. 将此功能放在一个间隔中,用户将在其表单身份验证过期时注销。 Voilà :-) An extra perk of this implementation is that you now have control over when the forms authentication's expiration gets extended. Voilà:-)这个实现的额外好处是,您现在可以控制表单身份验证的到期何时扩展。 If you want a bunch of web services that don't extend the expiration, just don't call the RenewAuthenticationTicket method for them. 如果您想要一堆不延长过期时间的Web服务,请不要为它们调用RenewAuthenticationTicket方法。

Please drop a comment if you have anything to add! 如果您有任何要添加的内容,请发表评论!

This can all be solved client side, without the need to go back to the server. 这一切都可以在客户端解决,而无需返回服务器。

In JavaScript do this. 在JavaScript中执行此操作。

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

The timeout will be set to 20 minutes on every page refresh. 每次刷新页面时,超时时间设置为20分钟。 This ensures that the user needs to get all their work done before the timeout happens. 这可确保用户需要在超时发生之前完成所有工作。 A lot of sites use this method, and it saves you from doing unnecessary server requests. 很多站点都使用这种方法,它可以避免你做不必要的服务器请求。

Your website functionality should work without JavaScript or you just replace one problem with another. 您的网站功能应该在没有JavaScript的情况下运行,或者您只需将一个问 I have tackled this problem also and here is how it was solved: 我也解决了这个问题,以下是它的解决方法:

When you authenticate yourself then session cookie is created with default lifetime on 20 min. 当您对自己进行身份验证时,将创建会话cookie,默认生命周期为20分钟。 When this expires user will be logged out. 当此过期时,用户将被注销。

饼干图像

When user selects "remember me" in the sign in form then additional persistence cookie [AuthCookie] is created in client side and in the database. 当用户在登录表单中选择“记住我”时,会在客户端和数据库中创建其他持久性cookie [AuthCookie]。 This cookie has a lifetime of 1 month. 这个cookie的生命周期为1个月。 Whenever page is loaded, session and persistence cookie data is recreated with a new lifetime (normally you want to decrypt/crypt the ticket). 每当加载页面时,会使用新生命周期重新创建会话和持久性cookie数据(通常您希望解密/加密票证)。

Imagine the following scenario. 想象一下以下场景。 A user has worked in our application for a while and then proceeds to do some other things, leaving our application idle for 20 minutes. 用户已经在我们的应用程序中工作了一段时间,然后继续做其他事情,让我们的应用程序闲置20分钟。 The user then returns to our application to write a report. 然后,用户返回到我们的应用程序以编写报告。 When the user tries to save, his session is restored before the request. 当用户尝试保存时,会话将在请求之前恢复。

One way to do this is to extend global.aspx to handle prerequest. 一种方法是扩展global.aspx以处理预先请求。 Something in the lines of: 有些东西:

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

AuthenticateUserFromExternalSource should check if cookie data matches with the database one, because anything stored in client side can be changed. AuthenticateUserFromExternalSource应检查cookie数据是否与数据库数据匹配,因为可以更改存储在客户端的任何内容。 If you have paid services with access rights then you need to check if user still has those rights and then you can recreate the session. 如果您具有访问权限的付费服务,则需要检查用户是否仍具有这些权限,然后您可以重新创建会话。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM