C# ASP.NET Coreでログインセッションのタイムアウトを実装し、非活動状態が続いた場合に「セッションが切れました」というメッセージを表示してログイン画面に自動遷移させる方法についてです。
この要件を実現するには、サーバーサイドのセッションタイムアウト設定と、クライアントサイド(JavaScript)での非活動監視を組み合わせる必要があります。
以下に、Program.cs
、_Layout.cshtml
、および Views/AdminUsers/Index.cshtml
の修正点を示します。
1. Program.cs
の修正
Cookie認証とセッションのタイムアウト時間を1分に設定し、セッション切れでログインページにリダイレクトされる際に、JavaScriptで検知できるように特別なクエリパラメータを追加します。
using ASPNETSQLServer.Service;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication; // 追加済み
using Microsoft.AspNetCore.Session; // 追加済み
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllersWithViews();
builder.Services.AddSingleton<PasswordHash>();
// 認証サービスを追加
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.LoginPath = "/AdminUsers/Index"; // ログインページへのパス
options.LogoutPath = "/AdminUsers/Logout"; // ログアウト処理のパス
options.AccessDeniedPath = "/AdminUsers/AccessDenied"; // アクセス拒否時のパス(任意で作成)
options.ExpireTimeSpan = TimeSpan.FromMinutes(1); // ★ Cookieの有効期限を1分に設定
options.SlidingExpiration = true; // アクセスがあるたびに有効期限を延長するかどうか
options.Cookie.HttpOnly = true; // JavaScriptからのアクセスを禁止(セッションハイジャック対策)
options.Cookie.IsEssential = true; // GDPR対応など、セッションCookieが必須であることを示す
options.Cookie.SecurePolicy = CookieSecurePolicy.Always; // HTTPSのみでCookieを送信(クライアントサイドセッション保護)
options.Cookie.SameSite = SameSiteMode.Lax; // CSRF対策
// セッションハイジャック対策: IPアドレスとユーザーエージェントの検証
options.Events = new CookieAuthenticationEvents
{
OnValidatePrincipal = async context =>
{
var currentIpAddress = context.HttpContext.Connection.RemoteIpAddress?.ToString();
var currentUserAgent = context.HttpContext.Request.Headers["User-Agent"].ToString();
var storedIpAddress = context.Principal.FindFirst("ClientIpAddress")?.Value;
var storedUserAgent = context.Principal.FindFirst("UserAgent")?.Value;
// ログアウトまたは認証情報が不足している場合は処理をスキップ
if (!context.Principal.Identity.IsAuthenticated || string.IsNullOrEmpty(storedIpAddress) || string.IsNullOrEmpty(storedUserAgent))
{
return;
}
// IPアドレスまたはユーザーエージェントが変更された場合、セッションを破棄
if (currentIpAddress != storedIpAddress || currentUserAgent != storedUserAgent)
{
context.RejectPrincipal(); // 認証を拒否
await context.HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); // Cookieを削除
context.HttpContext.Session.Clear(); // セッションデータをクリア
context.HttpContext.Response.Redirect(options.LoginPath); // ログインページへリダイレクト
return;
}
},
// OnRedirectToLogin イベントを追加して、リダイレクト時にクエリパラメータを追加
OnRedirectToLogin = context =>
{
// セッション切れによるリダイレクトの場合、特定のクエリパラメータを追加
// このイベントは、認証されていないユーザーが保護されたリソースにアクセスしようとしたときに発生します。
// セッションタイムアウトによるリダイレクトもこれに含まれます。
var returnUrl = context.Request.PathBase + context.Request.Path + context.Request.QueryString;
context.Response.Redirect(context.RedirectUri + "?sessionExpired=true&ReturnUrl=" + Uri.EscapeDataString(returnUrl));
return Task.CompletedTask;
}
};
});
// セッションサービスを追加
builder.Services.AddSession(options =>
{
options.IdleTimeout = TimeSpan.FromMinutes(1); // ★ セッションのタイムアウト時間を1分に設定
options.Cookie.HttpOnly = true; // JavaScriptからのアクセスを禁止(セッションハイジャック対策)
options.Cookie.IsEssential = true; // GDPR対応など、セッションCookieが必須であることを示す
options.Cookie.SecurePolicy = CookieSecurePolicy.Always; // HTTPSのみでCookieを送信(クライアントサイドセッション保護)
options.Cookie.SameSite = SameSiteMode.Lax; // CSRF対策
// セッションクッキーの名前を変更して、一般的なセッションIDを隠蔽することも検討できます。
// options.Cookie.Name = "MyAppSession";
});
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
// 認証ミドルウェアとセッションミドルウェアをUseAuthorization()の前に配置
app.UseAuthentication(); // 認証ミドルウェア
app.UseSession(); // セッションミドルウェア
app.UseAuthorization();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.MapControllerRoute(
name: "AdminUsers",
pattern: "{controller=AdminUsers}/{action=Index}/"
);
app.Run();
変更点:
options.ExpireTimeSpan
と options.IdleTimeout
をTimeSpan.FromMinutes(1)
に設定しました。
options.Events
にOnRedirectToLogin
イベントハンドラーを追加しました。
これにより、認証されていないユーザーが保護されたリソースにアクセスしようとしてログインページにリダイレクトされる際に、URLに sessionExpired=true というクエリパラメータが付加されます。これは、JavaScriptでセッション切れを検知するために使用します。
2. _Layout.cshtml
の修正
すべてのページに共通のJavaScriptコードを追加し、ユーザーの非活動状態を監視します。
一定時間操作がない場合、カスタムモーダルダイアログを表示し、その後ログインページへ自動的にリダイレクトします。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - ASPNETSQLServer</title>
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
<link rel="stylesheet" href="~/ASPNETSQLServer.styles.css" asp-append-version="true" />
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<style>
/* カスタムモーダルダイアログのスタイル */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
visibility: hidden; /* 初期状態では非表示 */
opacity: 0;
transition: visibility 0s, opacity 0.3s ease-in-out;
}
.modal-overlay.show {
visibility: visible;
opacity: 1;
}
.modal-content {
background-color: #fff;
padding: 30px;
border-radius: 8px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
text-align: center;
max-width: 400px;
width: 90%;
}
.modal-content h2 {
margin-top: 0;
color: #d9534f;
}
.modal-content p {
margin-bottom: 20px;
font-size: 1.1em;
color: #333;
}
.modal-content button {
background-color: #007bff;
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
font-size: 1em;
transition: background-color 0.3s ease;
}
.modal-content button:hover {
background-color: #0056b3;
}
</style>
</head>
<body>
<header>
<nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
<div class="container-fluid">
<a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">ASPNETSQLServer</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target=".navbar-collapse" aria-controls="navbarSupportedContent"
aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
<ul class="navbar-nav flex-grow-1">
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Home</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="AdminUsers" asp-action="Index">Admin Login</a>
</li>
@if (User.Identity.IsAuthenticated)
{
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="AdminUsers" asp-action="Dashboard">Dashboard</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="AdminUsers" asp-action="Logout">Logout</a>
</li>
}
</ul>
</div>
</div>
</nav>
</header>
<div class="container">
<main role="main" class="pb-3">
@RenderBody()
</main>
</div>
<footer class="border-top footer text-muted">
<div class="container">
© 2025 - ASPNETSQLServer - <a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
</div>
</footer>
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script>
<!-- カスタムモーダルダイアログのHTML -->
<div class="modal-overlay" id="sessionExpiredModal">
<div class="modal-content">
<h2>セッション切れ</h2>
<p>操作がなかったため、セッションが切れました。</p>
<p>自動的にログイン画面へ遷移します。</p>
</div>
</div>
<script>
// セッションタイムアウト時間 (Program.cs の ExpireTimeSpan と合わせる)
const SESSION_TIMEOUT_MS = 1 * 60 * 1000; // 1分 = 60000ミリ秒
let activityTimer;
// モーダルダイアログの表示/非表示関数
function showSessionExpiredModal() {
const modal = document.getElementById('sessionExpiredModal');
if (modal) {
modal.classList.add('show');
}
}
function hideSessionExpiredModal() {
const modal = document.getElementById('sessionExpiredModal');
if (modal) {
modal.classList.remove('show');
}
}
// タイマーをリセットする関数
function resetTimer() {
clearTimeout(activityTimer);
activityTimer = setTimeout(onSessionTimeout, SESSION_TIMEOUT_MS);
}
// セッションタイムアウト時に実行される関数
function onSessionTimeout() {
// モーダルを表示
showSessionExpiredModal();
// 数秒後にログインページへリダイレクト
setTimeout(() => {
// クエリパラメータを追加してログインページへリダイレクト
window.location.href = "/AdminUsers/Index?sessionExpired=true";
}, 3000); // 3秒後にリダイレクト
}
// ページロード時にタイマーを初期化
$(document).ready(function () {
// ユーザーが認証されている場合のみタイマーを起動
// このチェックは、未認証ユーザーがログインページにいるときにタイマーが不要なため
// ただし、ログインページでも「セッション切れ」メッセージを表示するために、
// ログインページではこのタイマーは起動しないようにします。
// ログインページは _Layout を使うので、このチェックは重要です。
const currentPath = window.location.pathname;
if (!currentPath.includes("/AdminUsers/Index") && !currentPath.includes("/AdminUsers/Login")) {
resetTimer();
// ユーザーの操作を監視し、操作があるたびにタイマーをリセット
$(document).on('mousemove keydown click', function () {
resetTimer();
});
}
});
</script>
@await RenderSectionAsync("Scripts", required: false)
</body>
</html>
変更点:
カスタムモーダルダイアログのHTMLとCSS: _Layout.cshtml の
内に、セッション切れメッセージを表示するためのシンプルなモーダルダイアログのHTML構造と、そのスタイルを追加しました。JavaScriptの追加:
SESSION_TIMEOUT_MS:
Program.cs で設定したタイムアウト時間(1分)をミリ秒単位で定義します。
showSessionExpiredModal() / hideSessionExpiredModal():
モーダルダイアログの表示/非表示を制御する関数です。
resetTimer():
ユーザーがマウス移動、キーボード入力、クリックなどの操作を行うたびに呼び出され、非活動タイマーをリセットします。
onSessionTimeout():
タイマーがタイムアウトした際に実行されます。カスタムモーダルを表示し、3秒後にログインページ (/AdminUsers/Index?sessionExpired=true) へリダイレクトします。
$(document).ready() 内で、現在のパスがログインページでない場合にのみ非活動監視タイマーを起動するようにしました。これにより、ログインページで不要なタイマーが動作するのを防ぎます。
3. Views/AdminUsers/Index.cshtml の修正
ログインページでURLのクエリパラメータsessionExpired=true
をチェックし、セッション切れのメッセージをカスタムモーダルで表示します。
ログインページでURLのクエリパラメータ sessionExpired=true をチェックし、セッション切れのメッセージをカスタムモーダルで表示します。
@{
ViewData["Title"] = "ログイン";
}
<h1>管理者ログイン</h1>
@if (!ViewData.ModelState.IsValid)
{
<div class="alert alert-danger">
<ul>
@foreach (var modelState in ViewData.ModelState.Values)
{
foreach (var error in modelState.Errors)
{
<li>@error.ErrorMessage</li>
}
}
</ul>
</div>
}
<form asp-controller="AdminUsers" asp-action="AdminUserLogin" method="post">
<div class="form-group">
<label for="AdminUserName">ユーザー名:</label>
<input type="text" class="form-control" id="inputName" name="AdminUserName" required>
</div>
<div class="form-group">
<label for="AdminUserEmail">メールアドレス:</label>
<input type="email" class="form-control" id="inputEmail" name="AdminUserEmail" required>
</div>
<div class="form-group">
<label for="AdminUserPassword">パスワード:</label>
<input type="password" class="form-control" id="inputPassword" name="AdminUserPassword" required>
</div>
<button type="submit" class="btn btn-primary">ログイン</button>
</form>
@section Scripts {
<script src="~/js/adminLogin.js"></script>
<script>
$(document).ready(function () {
// URLのクエリパラメータをチェック
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.has('sessionExpired') && urlParams.get('sessionExpired') === 'true') {
// カスタムモーダルダイアログを表示
showSessionExpiredModalOnLogin();
}
});
// ログインページ専用のモーダル表示関数
function showSessionExpiredModalOnLogin() {
const modal = document.getElementById('sessionExpiredModal');
if (modal) {
// モーダルのテキストをログインページ用に調整
const modalContent = modal.querySelector('.modal-content');
if (modalContent) {
modalContent.innerHTML = `
<h2>セッション切れ</h2>
<p>セッションが切れました。再度ログインしてください。</p>
<button id="modalCloseButton" class="btn btn-primary">OK</button>
`;
$('#modalCloseButton').on('click', function() {
modal.classList.remove('show');
// URLからsessionExpiredパラメータを削除してクリーンアップ
const url = new URL(window.location.href);
url.searchParams.delete('sessionExpired');
// ReturnUrl も削除
url.searchParams.delete('ReturnUrl');
window.history.replaceState({}, document.title, url.toString());
});
}
modal.classList.add('show');
}
}
</script>
}
変更点:
@section Scripts 内にJavaScriptを追加しました。
ページロード時に URLSearchParams を使用して、URLに sessionExpired=true が存在するかをチェックします。
存在する場合、showSessionExpiredModalOnLogin() 関数を呼び出して、ログインページ用の「セッションが切れました」メッセージを表示するモーダルを表示します。
モーダルの「OK」ボタンをクリックすると、モーダルが閉じ、URLから sessionExpired および ReturnUrl クエリパラメータを削除してURLをクリーンアップします。
全体的な動作
ログイン成功: ユーザーがログインに成功すると、認証Cookieが発行され、セッションが開始されます。Dashboard ページにリダイレクトされます。
非活動監視 (クライアントサイド): _Layout.cshtml に埋め込まれたJavaScriptが、ユーザーの操作(マウス移動、キーボード入力、クリックなど)を監視します。
クライアントサイドでのタイムアウト: ユーザーが1分間操作を行わないと、JavaScriptのタイマーがタイムアウトし、onSessionTimeout() 関数が実行されます。
メッセージ表示とリダイレクト (クライアントサイド): onSessionTimeout() はカスタムモーダルダイアログを表示し、「操作がなかったため、セッションが切れました。自動的にログイン画面へ遷移します。」とユーザーに通知します。その後、3秒後にJavaScriptはログインページ (/AdminUsers/Index?sessionExpired=true) へリダイレクトします。
ログインページでのメッセージ表示: リダイレクトされたログインページ (Index.cshtml) がロードされると、JavaScriptがURLの sessionExpired=true クエリパラメータを検出し、ログインページ専用の「セッションが切れました。再度ログインしてください。」というメッセージのモーダルを表示します。
SignOutAsync メソッドについて
SignOutAsync メソッドは、Microsoft.AspNetCore.Authentication.AuthenticationHttpContextExtensions クラスで定義されている静的拡張メソッドです。
このメソッドは、HttpContext クラスを拡張しているため、HttpContext インスタンスに対してあたかもメンバーメソッドであるかのように呼び出すことができます。
しかし、その拡張メソッドの定義が含まれる名前空間が using ディレクティブでインポートされていないと、コンパイラはそのメソッドを見つけることができません。
Microsoft.AspNetCore.Authentication を追加することで、コンパイラが SignOutAsync の定義を見つけられるようになり、エラーが解消されます。
以上です。