ASP.NET表单身份验证超时

16
这可能是一个非常简单的问题,但在尝试理解ASP.NET 4.0上的工作方式几个小时后,我仍然不知道。
我正在使用表单身份验证。 我有一个具有登录控件的登录页面。
以下是用户登录时需要的内容:
A-用户应该保持已登录状态,直到超时设置为止。 如果他们重新加载页面,则超时必须重新启动倒计时。
B-如果他们点击“记住我”复选框,则无论他们是否关闭浏览器或重新启动计算机,他们都应该保持连接状态,直到他们注销。
我遇到的问题是当他们登录时,我在我的电脑上看不到任何cookie:
1. cookie在哪里? 是内存cookie吗?
2. 如果会话过期会发生什么? 我想让他们保持登录,除非超时已完成。
3. 如果应用程序池被回收会发生什么?
另外我还有一个问题:当他们单击“记住我”复选框(情况B)时,我希望他们保持登录状态,直到单击注销按钮。 这次我确实看到了一个cookie,但它看起来他们只保持到了超时 ...那么记住我和没有记住我之间有什么区别呢...
我希望完全将身份验证和会话分开。 如果不是非常糟糕的方法,我希望通过cookie来控制身份验证。
感谢您的帮助。
2个回答

29

处理非永久性、滑动过期的票证

表单身份验证在票证使用内存中的 cookie,除非您将其设置为持久性(例如:FormsAuthentication.SetAuthCookie(username, true) 将使其具有持久性)。默认情况下,票证使用滑动过期。每次处理请求时,票证将随着新的到期日期一起发送。一旦过期,cookie 和票证都无效,用户将被重定向到登录页面。

表单身份验证没有内置功能来重定向已经渲染的页面,而这些页面停留的时间超过了超时时间。您需要自己添加此功能。最简单的级别是,在文档加载时使用 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 服务器以保持最新时,您需要小心,因为您不希望在他们移动光标或单击某些内容时向服务器发送大量请求。以下是添加到上面的 init 脚本中的 resetRedirect,它可以响应文档上的鼠标单击事件,以及初始页面加载和 AJAX 请求。
$(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. 您应该考虑角色是否应该被缓存在客户端。

关于第二点的详细说明:

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

从技术上讲,票证本身也存在相同的问题。同样,您不应该信任只是因为他们有有效的票证就意味着账户仍然有效。您可以使用与角色类似的逻辑:通过查询实际数据库来检查票证所引用的用户是否仍然存在且有效(即没有被锁定、禁用或删除),并将数据库结果缓存一段时间以提高性能。这就是我在自己的应用程序中所做的,其中票证被视为身份声明(类似地,用户名/密码是另一种类型的声明)。以下是在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会在会话过期或池重启后消失,对吗?为了创建一个持久的cookie,我应该把FormsAuthentication.SetAuthCookie(username, true)放在哪里? - John Mathison
此外,我不会依赖Session状态来处理与安全相关的事情。Session可以在任意时间开始和结束,在应用程序池被回收时终止(除非您使用单独的进程或数据库来存储Session状态),并且会对ASP.NET用于管理用户身份验证的机制产生负面影响。 - moribvndvs
谢谢!现在很清楚了。关于另一个问题,当用户勾选“记住我”时,如何让他们一直保持登录状态?我希望他们一直保持登录状态,直到他们点击注销。 - John Mathison
谢谢HackedByChinese!!!你帮了我很多。另一个问题是,由于我只使用Forms身份验证来存储用户,而我正在使用自己的用户数据库而不使用任何提供程序,是否有一种方法将用户的角色存储在cookie中?这样,每次检查用户是否在角色中时,我就不必访问数据库,可以使用提供Forms身份验证的User.IsInRole(RoleName)。您认为这样做更好还是每次访问数据库检查用户是否具有特定角色对性能没有太大影响... - John Mathison
哇,谢谢。现在我明白了,这是一种不好的方法,因为正如你所说,你的 cookie 上可能有旧信息。嗯,我喜欢每次都向数据库询问用户是否在角色中,这只是对数据库进行简单的检查。此外,这是一个内容管理系统,所以没有太多用户在其中操作。 - John Mathison
显示剩余3条评论

1

对于A,您需要将会话超时变量设置为您希望用户保持登录状态的时间长度(Timeout属性指定应用程序的Session对象分配的超时期限,以分钟为单位。如果用户在超时期限内不刷新或请求页面,则会话结束。)

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

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


谢谢。对于A:我想要分离Session和Authentication,我不希望Authentication与Session有任何关联。同时我发现通过设置slidingExpiration="1"(默认值),当用户进行操作时超时时间会重新计算。对于B:Session并不是一个解决方案,因为如果用户第二天回来,他必须再次登录,除非我将Session超时时间设置为超过1天,这并不是一个很好的方法。 - John Mathison
默认情况下为20分钟 - https://learn.microsoft.com/en-us/previous-versions/iis/6.0-sdk/ms525473(v=vs.90)?redirectedfrom=MSDN#parameters 上说 "默认值为10分钟"。 - David Klempfner

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接