处理非永久的、可滑动的过期票据
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 中,这样您就不必再次查找它们。是的,这是可能的,但有一些注意事项。
- 您应该限制放入票证的数据量,因为 cookie 只能这么大
- 您应该考虑是否应在客户端缓存角色。
详细说明#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);
}
}