0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【C#】ASP.NET Coreでセッション切れを管理する

Last updated at Posted at 2025-07-27

C# ASP.NET Coreでログインセッションのタイムアウトを実装し、非活動状態が続いた場合に「セッションが切れました」というメッセージを表示してログイン画面に自動遷移させる方法についてです。

この要件を実現するには、サーバーサイドのセッションタイムアウト設定と、クライアントサイド(JavaScript)での非活動監視を組み合わせる必要があります。

以下に、Program.cs_Layout.cshtml、および Views/AdminUsers/Index.cshtml の修正点を示します。

1. Program.cs の修正

Cookie認証とセッションのタイムアウト時間を1分に設定し、セッション切れでログインページにリダイレクトされる際に、JavaScriptで検知できるように特別なクエリパラメータを追加します。

Program.cs
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コードを追加し、ユーザーの非活動状態を監視します。
一定時間操作がない場合、カスタムモーダルダイアログを表示し、その後ログインページへ自動的にリダイレクトします。

_Layout.cshtml
<!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">
            &copy; 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 をチェックし、セッション切れのメッセージをカスタムモーダルで表示します。

Views/AdminUsers/Index.cshtml
@{
    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 の定義を見つけられるようになり、エラーが解消されます。

以上です。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?