简体   繁体   English

ASP.NET Forms身份验证超时

[英]ASP.NET Forms Authentication timeout

This could be a very simple question, but after a few hours trying to understand how this works on ASP.NET 4.0 I still don't know. 这可能是一个非常简单的问题,但在几个小时后试图理解它在ASP.NET 4.0上是如何工作的,我仍然不知道。

I'm using Forms Authentication. 我正在使用表单身份验证。 I have a login page with a login control on it. 我有一个登录页面,上面有登录控件。

This is what I need when users login: 这是用户登录时所需要的:

A- The users should stay logged until the don't do anything for the timeout set. A-用户应该保持记录状态,直到对超时设置不做任何操作。 If they reload a page the timeout has to restart the countdown. 如果他们重新加载页面,则超时必须重新开始倒计时。

B- If they click the "Remember Me" check they should stay connected until they logout, no matter if they close the browser or reboot the computer. B-如果他们点击“记住我”检查他们应该保持联系,直到他们退出,无论他们是关闭浏览器还是重新启动计算机。

The problem I have is when they login I don't see any cookie on my computer: 我遇到的问题是他们登录时我的计算机上没有看到任何cookie:

  1. Where is the cookie? 饼干在哪里? Is a memory cookie? 是内存cookie吗?
  2. What happens if the session expires? 如果会话到期会发生什么? I'd like to keep them logged unless the timeout is done. 除非超时完成,否则我想让它们保持记录状态。
  3. What happens if the app pool is recycled? 如果应用程序池被回收会发生什么?

Also I have another problem: when they click the "remember me" check (case B) I'd like them logged until they click on the logout button. 另外我还有另一个问题:当他们点击“记住我”检查(案例B)时,我希望他们记录,直到他们点击退出按钮。 This time I do see a cookie, but it looks like they stay connected only for the timeout...so what is the difference between the rememeber me or not... 这次我确实看到了一个cookie,但看起来它们只是为了超时而保持连接...所以记住我与否的区别是什么......

I'd like to separate completely Authentication and Session. 我想完全分开身份验证和会话。 I'd like Authentication controlled by cookies if is not very bad approaching. 我希望身份验证由cookie控制,如果接近不是很糟糕。

Thanks for helping-. 谢谢你的帮助。

Handling Non-Permanent, Sliding Expiration Tickets 处理非永久性滑动到期票

Forms Authentication uses an in-memory cookie for the ticket, unless you make it persistent (for example, FormsAuthentication.SetAuthCookie(username, true) will make it persistent). 表单身份验证使用内存中的cookie作为故障单,除非您使其持久化(例如, FormsAuthentication.SetAuthCookie(username, true)将使其持久)。 By default, the ticket uses a sliding expiration. 默认情况下,票证使用滑动到期。 Each time a request is processed, the ticket will be sent down with a new expiration date. 每次处理请求时,票证都将以新的到期日期发送。 Once that date expires, the cookie and the ticket are both invalid and the user will be redirected to the login page. 该日期到期后,cookie和故障单都无效,用户将被重定向到登录页面。

Forms Authentication has no built-in handling for redirecting pages that have already been rendered, that sit longer than the timeout. 表单身份验证没有内置处理方法来重定向已经呈现的页面,其长度超过超时。 You will need to add this yourself. 您需要自己添加。 At the simplest level, you will need to start a timer with the document loads, using JavaScript. 在最简单的级别,您将需要使用JavaScript启动文档加载的计时器。

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

With the above, if your page is not refreshed or changed, or redirectTimeoutHandle is not otherwise cancelled (with clearTimeout(redirectTimeoutHandle); ), it will be redirected to the login page. 如上所述,如果您的页面没有刷新或更改,或者redirectTimeoutHandle没有被取消(使用clearTimeout(redirectTimeoutHandle); ),它将被重定向到登录页面。 The FormsAuth ticket should have already expired so you shouldn't have to do anything with that. FormsAuth票证应已经过期,因此您不必对此进行任何操作。

The trick here is whether or not your site does AJAX work, or you consider other client-side events as active user activity (moving or clicking the mouse, etc). 这里的诀窍是您的站点是否可以运行AJAX,或者您将其他客户端事件视为活动用户活动(移动或单击鼠标等)。 You will have to track those events manually and when they occur, reset the redirectTimeoutHandle . 您必须手动跟踪这些事件,并在发生这些事件时重置redirectTimeoutHandle For example, I have a site that uses AJAX heavily, so the page doesn't physically refresh often. 例如,我有一个大量使用AJAX的站点,因此页面不会经常进行物理刷新。 Since I use jQuery, I can have it reset the timeout every time an AJAX request is issued, which should, in effect, result in the page being redirected if they sit on a single page and don't do any updates. 由于我使用jQuery,我可以让它在每次发出AJAX请求时重置超时,这实际上应该导致页面被重定向,如果它们位于单个页面上并且不进行任何更新。

Here's a complete initialization script. 这是一个完整的初始化脚本。

$(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.
});

By simply sending an AJAX request, the client-side timeout and the ticket (in the form of a cookie) will both be updated, and your user should be fine. 通过简单地发送一个AJAX请求,客户端超时和票证(以cookie的形式)都将被更新,您的用户应该没问题。

However, if user activity does not cause the FormsAuth ticket to be updated, the user will appear to be logged out the next time they request a new page (either by navigating or via AJAX). 但是,如果用户活动不会导致更新FormsAuth故障单,则用户下次请求新页面时(通过导航或通过AJAX)似乎将被注销。 In that case, you'll need to "ping" your web application when user activity occurs with an AJAX call to, say, a custom handler, an MVC action, etc. to keep your FormsAuth ticket up to date. 在这种情况下,当用户活动发生时,您需要通过AJAX调用(例如,自定义处理程序,MVC操作等)来“ping”您的Web应用程序,以使您的FormsAuth票证保持最新。 Please note that you need to be careful when pinging the server to keep up-to-date, as you don't want to flood the server with requests as they, say, move the cursor around or click on things. 请注意,在ping服务器以保持最新时需要小心,因为您不希望服务器充满请求,例如,移动光标或单击事物。 Here's an addition to the init script above that adds resetRedirect to mouse clicks on the document, in addition to the initial page load and AJAX requests. 除了初始页面加载和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.
   });
});

Handling "Permanent" Tickets 处理“永久”门票

You need the ticket to be sent to the client as a persistent cookie, with an arbitrarily long timeout. 您需要将票证作为持久性cookie发送到客户端,并具有任意长的超时。 You should be able to leave the client code and web.config as they are, but handle the user's preference for a permanent ticket separately in your login logic. 您应该可以保留客户端代码和web.config,但是在登录逻辑中单独处理用户对永久票证的偏好。 Here, you'll need to modify the ticket. 在这里,您需要修改故障单。 Below is logic in a login page to do such a thing: 下面是登录页面中的逻辑做这样的事情:

// 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);
}

Bonus: Storing Roles in Ticket 奖励:在票证中存储角色

You asked if you can store roles in the ticket/cookie so you don't have to look them up again. 您询问是否可以在故​​障单/ cookie中存储角色,因此您无需再次查找它们。 Yes, that is possible, but there are some considerations. 是的,这是可能的,但有一些考虑因素。

  1. You should limit the amount of data you put in the ticket, because cookies can only be so large 您应该限制您在故障单中放入的数据量,因为cookie只能是这么大
  2. You should consider whether or not roles should be cached at the client. 您应该考虑是否应该在客户端缓存角色。

To elaborate on #2: 详细说明#2:

You shouldn't implicitly trust claims that you receive from a user. 您不应该隐含地信任您从用户那里收到的声明。 For example, if a user logs in and is an Admin, and checks "remember me" thus receiving a persistent, long-term ticket, they will be an Admin forever (or until that cookie expires or is erased). 例如,如果用户登录并且是管理员,并且检查“记住我”从而接收持久的长期票证,则他们将永远是管理员(或者直到该cookie过期或被删除)。 If someone removes them from that role in your database, the application will still think they are an Admin if they have the old ticket. 如果有人在数据库中将其从该角色中删除,则应用程序仍然认为如果他们拥有旧票证,则他们是管理员。 So, you may be better off getting the user's roles every time, but caching the roles in the application instance for a period of time to minimize database work. 因此,您最好每次获取用户的角色,但在应用程序实例中缓存角色一段时间以最大限度地减少数据库工作。

Technically, this is also an issue for the ticket itself. 从技术上讲,这也是机票本身的问题。 Again, you shouldn't trust that just because they have a valid ticket that the account is still valid. 同样,您不应该相信这只是因为他们拥有该帐户仍然有效的有效票证。 You can use similar logic as the roles: Check that the user referenced by the ticket still exists and is valid (that it's not locked out, disabled, or deleted) by querying your actual database, and just caching the db results for a period of time to improve performance. 您可以使用与角色类似的逻辑:通过查询实际数据库检查故障单引用的用户是否仍然存在并且是否有效(它没有被锁定,禁用或删除),只需将数据库缓存缓存一段时间时间来提高绩效。 This is what I do in my applications, where the ticket is treated as an identity claim (similarly, username/password is another type of claim). 这就是我在我的应用程序中所做的事情,其中​​票证被视为身份声明(类似地,用户名/密码是另一种类型的声明)。 Here is simplified logic in the global.asax.cs (or in an HTTP module): 这是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;
}

Of course, you may decide you don't care about any of that and just want to trust the ticket, and store the roles in the ticket as well. 当然,您可能决定不关心任何这一点,只想信任票证,并将角色存储在票证中。 First, we update your login logic from above: 首先,我们从上面更新您的登录逻辑:

// 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));
}

Add method: 添加方法:

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

Update your global.asax.cs to get roles out of ticket and update HttpContext.User: 更新您的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);
  }
}

For A you will want to set the session timeout variable to however long you want the user to stay logged in for The Timeout property specifies the time-out period assigned to the Session object for the application, in minutes. 对于A,您需要将会话超时变量设置为您希望用户保持登录的时间长度。超时属性指定分配给应用程序的Session对象的超时时间(以分钟为单位)。 If the user does not refresh or request a page within the time-out period, the session ends. 如果用户在超时期限内没有刷新或请求页面,则会话结束。

For part B I would suggest storing that value in a session variable(or cookie but that doesn't reside on the server) and checking for that value in the Session_End event in the global.asax file. 对于B部分,我建议将该值存储在会话变量(或cookie但不驻留在服务器上)中,并在global.asax文件中的Session_End事件中检查该值。 If it is set then renew the session. 如果已设置,则续订会话。

The Session_End event doesn't fire when the browser is closed, it fires when the server hasn't gotten a request from the user in a specific time period (by default 20 minutes). 当浏览器关闭时,Session_End事件不会触发,当服务器在特定时间段内没有收到用户的请求时(默认为20分钟),它会触发。

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

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