【问题标题】:ASP.NET Forms Authentication timeoutASP.NET 表单身份验证超时
【发布时间】:2012-10-15 18:56:57
【问题描述】:

这可能是一个非常简单的问题,但经过几个小时试图了解它在 ASP.NET 4.0 上的工作原理后,我仍然不知道。

我正在使用表单身份验证。我有一个登录页面,上面有登录控件。

这是我在用户登录时需要的:

A- 用户应该保持登录状态,直到超时设置不做任何事情。如果他们重新加载页面,超时必须重新开始倒计时。

B- 如果他们单击“记住我”,则无论他们是关闭浏览器还是重新启动计算机,他们都应该保持连接直到他们注销。

我遇到的问题是,当他们登录时,我的计算机上没有看到任何 cookie:

  1. cookie 在哪里?是记忆饼干吗?
  2. 如果会话过期会怎样?除非超时完成,否则我希望记录它们。
  3. 如果应用程序池被回收会怎样?

我还有另一个问题:当他们点击“记住我”检查(案例 B)时,我希望他们登录,直到他们点击注销按钮。这次我确实看到了一个 cookie,但看起来他们只是在超时后才保持连接……所以记住我与不记住我有什么区别……

我想将身份验证和会话完全分开。如果不是很糟糕,我希望通过 cookie 控制身份验证。

感谢您的帮助-。

【问题讨论】:

    标签: asp.net timeout forms-authentication


    【解决方案1】:

    处理非永久的、可滑动的过期票据

    Forms Authentication 为票证使用内存中的 cookie,除非您使其持久化(例如,FormsAuthentication.SetAuthCookie(username, true) 将使其持久化)。默认情况下,票证使用滑动到期。每次处理请求时,都会发送带有新到期日期的票证。一旦该日期到期,cookie 和票证都无效,用户将被重定向到登录页面。

    Forms Authentication 没有内置处理重定向已经呈现的页面,这些页面的停留时间超过了超时时间。您需要自己添加。在最简单的层面上,您需要使用 JavaScript 在文档加载时启动一个计时器。

    <script type="text/javascript">
      var redirectTimeout = <%FormsAuthentication.Timeout.TotalMilliseconds%>
      var redirectTimeoutHandle = setTimeout(function() { window.location.href = '<%FormsAuthentication.LoginUrl%>'; }, redirectTimeout);
    </script>
    

    如果您的页面没有刷新或更改,或者redirectTimeoutHandle没有被取消(使用clearTimeout(redirectTimeoutHandle);),它将被重定向到登录页面。 FormsAuth 票证应该已经过期,所以您不必对此做任何事情。

    这里的诀窍是您的网站是否支持 AJAX,或者您是否将其他客户端事件视为活跃的用户活动(移动或单击鼠标等)。您必须手动跟踪这些事件,并在它们发生时重置redirectTimeoutHandle。例如,我有一个大量使用 AJAX 的网站,因此页面不会经常刷新。由于我使用 jQuery,我可以让它在每次发出 AJAX 请求时重置超时,实际上,如果它们位于单个页面上并且不进行任何更新,这应该会导致页面被重定向。

    这是一个完整的初始化脚本。

    $(function() {
       var _redirectTimeout = 30*1000; // thirty minute timeout
       var _redirectUrl = '/Accounts/Login'; // login URL
    
       var _redirectHandle = null;
    
       function resetRedirect() {
           if (_redirectHandle) clearTimeout(_redirectHandle);
           _redirectHandle = setTimeout(function() { window.location.href = _redirectUrl; }, _redirectTimeout);
       }
    
       $.ajaxSetup({complete: function() { resetRedirect(); } }); // reset idle redirect when an AJAX request completes
    
       resetRedirect(); // start idle redirect timer initially.
    });
    

    通过简单地发送 AJAX 请求,客户端超时和票证(以 cookie 的形式)都将被更新,您的用户应该没问题。

    但是,如果用户活动不会导致 FormsAuth 票证被更新,则用户在下一次请求新页面(通过导航或通过 AJAX)时将显示为已注销。在这种情况下,当用户通过 AJAX 调用(例如,自定义处理程序、MVC 操作等)发生用户活动时,您需要“ping”您的 Web 应用程序,以使您的 FormsAuth 票证保持最新。请注意,在 ping 服务器以保持最新状态时需要小心,因为您不希望服务器充斥着请求,例如移动光标或单击内容。除了初始页面加载和 AJAX 请求之外,上面的初始化脚本添加了 resetRedirect 到文档上的鼠标点击。

    $(function() {
       $(document).on('click', function() {
          $.ajax({url: '/ping.ashx', cache: false, type: 'GET' }); // because of the $.ajaxSetup above, this call should result in the FormsAuth ticket being updated, as well as the client redirect handle.
       });
    });
    

    处理“永久”票证

    您需要将票据作为持久 cookie 发送到客户端,并具有任意长的超时时间。您应该能够保留客户端代码和 web.config 原样,但在您的登录逻辑中单独处理用户对永久票证的偏好。在这里,您需要修改票证。以下是登录页面中执行此类操作的逻辑:

    // assumes we have already successfully authenticated
    
    if (rememberMe)
    {
        var ticket = new FormsAuthenticationTicket(2, userName, DateTime.Now, DateTime.Now.AddYears(50), true,
                                                   string.Empty, FormsAuthentication.FormsCookiePath);
        var cookie = new HttpCookie(FormsAuthentication.FormsCookieName, FormsAuthentication.Encrypt(ticket))
                         {
                             Domain = FormsAuthentication.CookieDomain,
                             Expires = DateTime.Now.AddYears(50),
                             HttpOnly = true,
                             Secure = FormsAuthentication.RequireSSL,
                             Path = FormsAuthentication.FormsCookiePath
                         };
        Response.Cookies.Add(cookie);
        Response.Redirect(FormsAuthentication.GetRedirectUrl(userName, true));
    }
    else
    {
        FormsAuthentication.RedirectFromLoginPage(userName, false);
    }
    

    奖励:在票证中存储角色

    您询问是否可以将角色存储在票证/cookie 中,这样您就不必再次查找它们。是的,这是可能的,但有一些注意事项。

    1. 您应该限制放入票证的数据量,因为 cookie 只能这么大
    2. 您应该考虑是否应在客户端缓存角色。

    详细说明#2:

    您不应暗中信任您从用户那里收到的声明。例如,如果用户登录并且是管理员,并选中“记住我”从而收到持久的长期票证,他们将永远是管理员(或直到该 cookie 过期或被删除)。如果有人将他们从数据库中的该角色中删除,如果他们有旧票证,应用程序仍会认为他们是管理员。因此,您最好每次都获取用户的角色,但将角色缓存在应用程序实例中一段时间​​以最小化数据库工作。

    从技术上讲,这也是票本身的问题。同样,您不应该仅仅因为他们拥有有效的票证而相信该帐户仍然有效。您可以使用与角色类似的逻辑:通过查询您的实际数据库来检查票证引用的用户是否仍然存在并且有效(它没有被锁定、禁用或删除),并且只是将 db 结果缓存一段时间是时候提高性能了。这就是我在我的应用程序中所做的,其中票证被视为身份声明(类似地,用户名/密码是另一种类型的声明)。以下是 global.asax.cs(或 HTTP 模块)中的简化逻辑:

    protected void Application_AuthenticateRequest(Object sender, EventArgs e)
    {
      var application = (HttpApplication)sender;
      var context = application.Context;  
    
      EnsureContextUser(context);
    }
    
    private void EnsureContextUser(HttpContext context)
    {
       var unauthorizedUser = new GenericPrincipal(new GenericIdentity(string.Empty, string.Empty), new string[0]);
    
       var user = context.User;
    
       if (user != null && user.Identity.IsAuthenticated && user.Identity is FormsIdentity)
       {
          var ticket = ((FormsIdentity)user.Identity).Ticket;
    
          context.User = IsUserStillActive(context, ticket.Name) ? new GenericPrincipal(user.Identity, GetRolesForUser(context, ticket.Name)) : unauthorizedUser;
    
          return; 
       }
    
       context.User = unauthorizedUser;
    }
    
    private bool IsUserStillActive(HttpContext context, string username)
    {
       var cacheKey = "IsActiveFor" + username;
       var isActive = context.Cache[cacheKey] as bool?
    
       if (!isActive.HasValue)
       {
          // TODO: look up account status from database
          // isActive = ???
          context.Cache[cacheKey] = isActive;
       }
    
       return isActive.GetValueOrDefault();
    }
    
    private string[] GetRolesForUser(HttpContext context, string username)
    {
       var cacheKey = "RolesFor" + username;
       var roles = context.Cache[cacheKey] as string[];
    
       if (roles == null)
       {
          // TODO: lookup roles from database
          // roles = ???
          context.Cache[cacheKey] = roles;
       }
    
       return roles;
    }
    

    当然,您可能决定不关心这些,只想信任票证,并将角色也存储在票证中。首先,我们从上面更新您的登录逻辑:

    // assumes we have already successfully authenticated
    
    if (rememberMe)
    {
        var ticket = new FormsAuthenticationTicket(2, userName, DateTime.Now, DateTime.Now.AddYears(50), true, GetUserRolesString(), FormsAuthentication.FormsCookiePath);
        var cookie = new HttpCookie(FormsAuthentication.FormsCookieName, FormsAuthentication.Encrypt(ticket))
                         {
                             Domain = FormsAuthentication.CookieDomain,
                             Expires = DateTime.Now.AddYears(50),
                             HttpOnly = true,
                             Secure = FormsAuthentication.RequireSSL,
                             Path = FormsAuthentication.FormsCookiePath
                         };
        Response.Cookies.Add(cookie);
        Response.Redirect(FormsAuthentication.GetRedirectUrl(userName, true));
    }
    else
    {
        var ticket = new FormsAuthenticationTicket(2, userName, DateTime.Now, DateTime.Now.AddMinutes(FormsAuthentication.Timeout), false, GetUserRolesString(), FormsAuthentication.FormsCookieName);
        var cookie = new HttpCookie(FormsAuthentication.FormsCookieName, FormsAuthentication.Encrypt(ticket))
           {
              Domain = FormsAuthentication.CookieDomain,
              HttpOnly = true,
              Secure = FormsAuthentication.RequireSSL,
              Path = FormsAuthentication.FormsCookiePath
           };
        Response.Cookies.Add(cookie);
        Response.Redirect(FormsAuthentication.GetRedirectUrl(userName, false));
    }
    

    添加方法:

       private string GetUserRolesString(string userName)
       {
            // TODO: get roles from db and concatenate into string
       }
    

    更新您的 global.asax.cs 以从票证中获取角色并更新 HttpContext.User:

    protected void Application_AuthenticateRequest(Object sender, EventArgs e)
    {
      var application = (HttpApplication)sender;
      var context = application.Context;  
    
      if (context.User != null && context.User.Identity.IsAuthenticated && context.User.Identity is FormsIdentity)
      {
          var roles = ((FormsIdentity)context.User.Identity).Ticket.Data.Split(",");
    
          context.User = new GenericPrincipal(context.User.Identity, roles);
      }
    }
    

    【讨论】:

    • 谢谢,我认为内存中的 cookie 会在会话过期或池回收后消失,对吧?我应该在哪里放置 FormsAuthentication.SetAuthCookie(username, true) 以创建持久 cookie?
    • 不,它由客户端的浏览器保存在内存中。只要 cookie 没有达到过期时间,即使应用程序池被回收,cookie 仍然有效。 Forms Auth 独立于会话状态,因此您对会话所做的任何事情都不会影响票证。至于创建 cookie/票证并使其持久化,您不必专门使用 SetAuthCookie。例如,您可以使用 RedirectFromLoginPage 的重载(参见 msdn.microsoft.com/en-us/library/ka5ffkce.aspx)。只需在您当前正在登录用户的地方使用它。
    • 另外,我不会依赖会话状态来处理与安全相关的内容。会话可以任意启动和结束,在应用程序池被回收时终止(除非您使用单独的进程或数据库来存储会话状态),并且与 ASP.NET 用于管理用户身份验证的机制相悖。
    • 谢谢!!!现在很清楚了。关于另一个问题,当用户标记“记住我”时,如何让用户“永远”登录?我想让他们登录,直到他们点击注销
    • 在 web.config 中,我设置了 &lt;forms sliding="true" timeout="525600" .. &gt;,其中 525600 可以是任意长的超时时间(以分钟为单位)。我刚选了525600,也就是一年,从我的头上掉下来。我认为给用户一年的时间来导航或刷新页面已经足够了!然后,只需确保使用 RedirectFromLoginPageSetAuthCookie 的重载,或任何可以指定 cookie 持久并将其设置为 true 的位置。即使关闭浏览器,这也会保留表单身份验证票。
    【解决方案2】:

    对于 A,您需要将会话超时变量设置为您希望用户在The Timeout property specifies the time-out period assigned to the Session object for the application, in minutes. If the user does not refresh or request a page within the time-out period, the session ends. 保持登录状态的时间长度

    对于 B 部分,我建议将该值存储在会话变量(或 cookie,但不驻留在服务器上)中,并在 global.asax 的 Session_End 事件中检查该值文件。如果已设置,则更新会话。

    Session_End 事件在浏览器关闭时不会触发,它会在服务器在特定时间段(默认为 20 分钟)内未收到用户请求时触发。

    【讨论】:

    • 谢谢。对于 A:我想将 Session 和 Authentication 分开,我不希望 Authentication 与 Session 无关。我还发现,通过设置slidingExpiration="1"(默认),当用户执行某些操作时,超时会重新开始。对于 B:会话不是解决方案,因为如果用户第二天回来,他必须再次登录,除非我将会话超时设置为 1 天以上,这不是很好的方法。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-07-22
    • 2014-07-13
    • 2017-09-28
    • 1970-01-01
    相关资源
    最近更新 更多