15

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

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

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

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

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

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

  1. 饼干在哪里?是记忆饼干吗?
  2. 如果会话过期会发生什么?我想让他们记录下来,除非超时完成。
  3. 如果应用程序池被回收会怎样?

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

我想完全分离身份验证和会话。如果不是很糟糕,我希望由 cookie 控制的身份验证。

感谢您的帮助-。

4

2 回答 2

28

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

Forms Authentication 为票证使用内存中的 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 服务器以保持最新状态时需要小心,因为您不想让服务器充斥着请求,例如移动光标或单击内容。除了初始页面加载和 AJAX 请求之外,这是对上面的 init 脚本的补充,它添加了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);
  }
}
于 2012-10-15T21:34:55.740 回答
1

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

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

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

于 2012-10-15T19:58:37.850 回答