非永続的なスライド式の有効期限チケットの処理
フォーム認証では、チケットを永続化しない限り(たとえば、永続化する場合を除いて)、チケットにメモリ内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リクエストが発行されるたびにタイムアウトをリセットすることができます。これにより、実際には、ページが1つのページにあり、更新を行わない場合、ページがリダイレクトされます。
これが完全な初期化スクリプトです。
$(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を介して)ログアウトしているように見えます。その場合、FormsAuthチケットを最新の状態に保つために、カスタムハンドラーやMVCアクションなどへのAJAX呼び出しでユーザーアクティビティが発生したときに、Webアプリケーションに「ping」を実行する必要があります。サーバーにpingを実行して最新の状態に保つ場合は、カーソルを移動したりクリックしたりするなどのリクエストでサーバーをあふれさせたくないので、注意する必要があることに注意してください。resetRedirect
これは、最初のページの読み込みとAJAXリクエストに加えて、ドキュメントのマウスクリックに追加する上記のinitスクリプトへの追加です。
$(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について詳しく説明するには:
ユーザーから受け取ったクレームを暗黙的に信頼するべきではありません。たとえば、ユーザーがログインして管理者であり、「remember me」をチェックして永続的な長期チケットを受け取った場合、そのユーザーは永久に(または、Cookieの有効期限が切れるか消去されるまで)管理者になります。誰かがデータベース内のそのロールからそれらを削除した場合でも、古いチケットを持っていれば、アプリケーションはそれらが管理者であると見なします。したがって、毎回ユーザーのロールを取得する方がよい場合がありますが、データベースの作業を最小限に抑えるために、アプリケーションインスタンスでロールを一定期間キャッシュします。
技術的には、これはチケット自体の問題でもあります。繰り返しになりますが、アカウントがまだ有効であるという有効なチケットを持っているという理由だけで、それを信頼するべきではありません。ロールと同様のロジックを使用できます。実際のデータベースにクエリを実行し、dbの結果を一定期間キャッシュすることで、チケットによって参照されるユーザーがまだ存在し、有効であること(ロックアウト、無効化、または削除されていないこと)を確認します。パフォーマンスを改善する時間。これは、チケットがIDクレームとして扱われるアプリケーションで行うことです(同様に、ユーザー名/パスワードは別のタイプのクレームです)。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);
}
}