1
1

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で管理者ログインのセッション管理を実装

Posted at

サンプルイメージ

ログイン画面でアカウント情報を入力すると、
image.png

ダッシュボードへ遷移するまでのサンプルです。
image.png

こちらをセッション管理を使って実装していきましょう。

使っているNETのバージョン

バージョンは6.0を使っています。

フロントの実装

Views/AdminUsers/Index.cshtml
@{
    ViewData["Title"] = "Login Page";
}
<script src="~/js/adminUsers/adminLogin.js"></script>

<div class="text-center">
    管理者ログインだよ
    @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>
        <div class="mb-3">
            <label for="inputName" class="form-label">名前</label>
            <input type="text" class="form-control" id="inputName" aria-describedby="emailHelp">
            <span></span>
        </div>
        <div class="mb-3">
            <label for="inputEmail" class="form-label">メールアドレス</label>
            <input type="email" class="form-control" id="inputEmail" aria-describedby="emailHelp">
            <span></span>
        </div>
        <div class="mb-3">
            <label for="inputPassword" class="form-label">パスワード</label>
            <input type="password" class="form-control" id="inputPassword">
        </div>
        <button type="button" class="btn btn-primary" id="adminUserLoginBtn" onclick="AdminUserLogin()">ログイン</button>
    </form>
</div>

Javascriptのコードはこちら⇩

adminUserLogin.js
function AdminUserLogin()
{
    const adminUserName = $("#inputName").val();
    const adminUserEmail = $("#inputEmail").val();
    const adminUserPassword = $("#inputPassword").val();
    
    let validateFlg = true;// 検証フラグ

    if(adminUserName === "" || adminUserName === "")
    {
        validateFlg = false;
    }
    if(adminUserEmail === "" || adminUserEmail === "")
    {
        validateFlg = false;
    }
    if(adminUserPassword === "" || adminUserPassword === "")
    {
        validateFlg = false;
    }
    if(validateFlg === false)
    {
        window.confirm("入力値が正しくありません。");
        return;
    }

    let formData = new FormData();
    formData.append('AdminUserName',adminUserName);
    formData.append('AdminUserEmail',adminUserEmail);
    formData.append('AdminUserPassword',adminUserPassword);

    $.ajax({
        url: "/AdminUsers/AdminUserLogin",
        type: "POST",
        data: formData, 
        processData: false,
        contentType: false,
        cache: false,
        success: function (data, textStatus, xhr) {
            if(xhr.status !=200){
                const message = data.message || "ログインに失敗しました。";
                alert(message);
            }
            window.location.href = "/AdminUsers/Dashboard";
        },
        error: function (xhr) {
            // エラーハンドリングを強化
            if (xhr.status === 401) { // Unauthorized (認証失敗)
                const errorMessage = xhr.responseJSON && xhr.responseJSON.Message ? xhr.responseJSON.Message : "ユーザー名、メールアドレス、またはパスワードが間違っています。";
                alert(errorMessage);
            } else if (xhr.status === 500) { // Internal Server Error
                alert("サーバーエラーが発生しました。時間をおいて再度お試しください。");
            } else {
                alert(`予期せぬエラーが発生しました。ステータス:${xhr.status}`);
            }
        }
    });
}
Views/AdminUsers/Dashboard.cshtml
@{
    ViewData["Title"] = "Dashboard";
}
<script src="~/js/adminUsers/adminLogin.js"></script>

<div class="text-center">
    <h1>Dashboard</h1>

    <p>ようこそ、@ViewBag.AdminUserName さん!</p>
    <p>メールアドレス: @ViewBag.AdminUserEmail</p>
    <a asp-controller="AdminUsers" asp-action="Logout" class="btn btn-danger">ログアウト</a>
</div>

サーバ側の実装

まずは、Program.csの実装です。

全体のコードはこちら⇩

Program.cs
using ASPNETSQLServer.Service;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Identity; // PasswordHash サービスのnamespace
using Microsoft.AspNetCore.Authentication;//追加した 2025.7.27

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddControllersWithViews();

builder.Services.AddSingleton<PasswordHash>();//追加

//認証サービスを追加 2025/7/27
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie(options =>
    {
        options.LoginPath = "/AdminUsers/Index";// ログインページへのパス
        options.LogoutPath = "/AdminUsers/Logout"; // ログアウト処理のパス
        options.AccessDeniedPath = "/AdminUsers/AccessDenied"; // アクセス拒否時のパス(任意で作成)
        options.ExpireTimeSpan = TimeSpan.FromMinutes(30); // Cookieの有効期限
        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;
                }
            }
        };
    });

// セッションサービスを追加 2025/7/27
builder.Services.AddSession(options =>
{
    options.IdleTimeout = TimeSpan.FromMinutes(30); // セッションのタイムアウト時間
    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");
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

// 認証ミドルウェアとセッションミドルウェアをUseAuthorization()の前に配置 2025.7.27
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();

Program.csの解説

ASP.NET Coreの認証・認可の仕組みとセッション管理を組み込む方法を見ていきましょう。ASP.NET Coreでは、Cookieベースの認証が推奨されており、セッション管理もこれと連携して利用するのが一般的です。

以下の説明では、ASP.NET Coreの標準的な認証ミドルウェアとセッションミドルウェアを使用します。

1. プロジェクトの準備と必要なパッケージ

まず、必要なパッケージがインストールされていることを確認します。ASP.NET CoreのWebプロジェクトであれば、通常、以下のパッケージは既に入っていますが、念のため確認してください。

Microsoft.AspNetCore.Authentication.Cookies

Microsoft.AspNetCore.Session

もしインストールされていない場合は、NuGetパッケージマネージャーからインストールしてください。

重要な設定項目とセキュリティ対策:

options.Cookie.HttpOnly = true;: JavaScriptからのCookieへのアクセスを禁止します。これにより、XSS(クロスサイトスクリプティング)攻撃によって悪意のあるスクリプトがCookieを盗むのを防ぎ、セッションハイジャック対策になります。

options.Cookie.SecurePolicy = CookieSecurePolicy.Always;: CookieをHTTPS接続の場合にのみ送信するように強制します。これにより、中間者攻撃によるCookieの傍受を防ぎ、クライアントサイドセッションの保護に貢献します。

options.Cookie.SameSite = SameSiteMode.Lax;: CSRF(クロスサイトリクエストフォージェリ)攻撃に対する基本的な保護を提供します。Lax は、トップレベルのナビゲーション(GETリクエスト)と一部のサードパーティリクエストではCookieを送信しますが、通常のPOSTフォームなどでは送信しません。より厳格な Strict もありますが、ユーザー体験に影響を与える可能性があります。

options.SlidingExpiration = true;: Cookieの有効期限をスライディング(延長)させます。ユーザーが活動している限りログイン状態を維持し、非活動状態が続いた場合にのみタイムアウトさせます。

options.IdleTimeout: セッションの非活動タイムアウトを設定します。この時間が経過すると、サーバー側のセッションデータが破棄されます。

options.LoginPath: 認証されていないユーザーが保護されたリソースにアクセスしようとしたときにリダイレクトされるログインページのパスを指定します。

options.Events.OnValidatePrincipal の実装:

このイベントは、各リクエストで認証チケット(Cookie内のプリンシパル)が検証される際に呼び出されます。

ここで、ログイン時にクレームとして保存されたIPアドレスとユーザーエージェントを現在のリクエストのものと比較します。

一致しない場合(セッションハイジャックの可能性)、context.RejectPrincipal() で認証を拒否し、SignOutAsync でCookieを削除、Session.Clear() でセッションデータをクリアし、ログインページにリダイレクトします。これにより、攻撃者が盗んだCookieを使ってアクセスするのを防ぎます。

セッションサービスの追加 (builder.Services.AddSession):

AddSession メソッドを呼び出し、サーバーサイドセッションを使用するための設定を行います。

options.IdleTimeout: セッションの非活動タイムアウトを設定します。

Cookieセキュリティ設定: セッションCookieも認証Cookieと同様に HttpOnly, SecurePolicy, SameSite を設定し、保護を強化します。

ミドルウェアの順序の変更:

app.UseAuthentication(); app.UseAuthorization(); の前に配置します。認証が先に行われ、その後に認可が行われる必要があります。

app.UseSession(); app.UseAuthorization(); の前に配置します。セッションは認証後のユーザーの状態を維持するために使用されるため、認証ミドルウェアの後に来るべきです。

AdminUserController.csの実装

つづいては、AdminUserController.csです。
全体のコードはこちら⇩

Controllers/AdminUserController.cs
using System.Diagnostics;
using Microsoft.AspNetCore.Mvc;
using ASPNETSQLServer.Models;
using Microsoft.Extensions.Configuration;//追加
using Microsoft.Data.SqlClient;
using System.Data.Common;//追加
using ASPNETSQLServer.Models;
using Microsoft.AspNetCore.Mvc.RazorPages;
using System.Data;
using ASPNETSQLServer.Service;//追加
using Microsoft.AspNetCore.Http;
using System.Security.Claims;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication; // 追加
using System.Security.Claims; // 追加
using Microsoft.AspNetCore.Authentication; // 追加
using Microsoft.AspNetCore.Authentication.Cookies; // 追加

namespace ASPNETSQLServer.Controllers;

public class AdminUsersController : Controller
{
    private readonly ILogger<AdminUsersController> _logger;
    private readonly string _connectString;
    private readonly PasswordHash passwordHash;
    public AdminUsersController(ILogger<AdminUsersController> logger, IConfiguration configuration, PasswordHash passwordHash)
    {
        _logger = logger;
        _connectString = configuration.GetConnectionString("SqlServerConnection");
        this.passwordHash = passwordHash;
    }
    [HttpGet]
    public IActionResult Index()
    {
        // 既にログインしている場合はDashboardにリダイレクト
        if (User.Identity.IsAuthenticated)
        {
            return RedirectToAction("Dashboard", "AdminUsers");
        }
        return View();
    }

    [HttpPost]
    public async Task<IActionResult> AdminUserLogin(AdminUserViewModel adminUserViewModel)
    {
        string selectQuery = "SELECT name,email,password FROM [test].[dbo].[adminusers] WHERE name = @name AND email = @email";

        List<AdminUserViewModel> adminUserViewModelsList = new List<AdminUserViewModel>();
        using (SqlConnection connection = new SqlConnection(_connectString))
        {
            using (SqlCommand command = new SqlCommand(selectQuery, connection))
            {
                command.Parameters.AddWithValue("@name", adminUserViewModel.AdminUserName);
                command.Parameters.AddWithValue("@email", adminUserViewModel.AdminUserEmail);
                try
                {
                    await connection.OpenAsync();
                    using (SqlDataReader reader = await command.ExecuteReaderAsync())
                    {
                        if (!reader.HasRows)
                        {
                            Object notFoundUser = new { status = 200, message = "登録しました。" };
                            return Json(notFoundUser);
                        }
                        while (await reader.ReadAsync())
                        {
                            AdminUserViewModel adminUser = new AdminUserViewModel
                            {
                                AdminUserName = reader["name"].ToString(),
                                AdminUserEmail = reader["email"].ToString(),
                                AdminUserPassword = reader["password"].ToString(),
                            };
                            adminUserViewModelsList.Add(adminUser);
                        }
                    }
                }
                catch (DbException dbException)
                {
                    Console.WriteLine($"データベースエラーが発生しました。。エラーメッセージ:{dbException}");
                }
                // 取得したデータに基づいて処理を行う
                if (adminUserViewModelsList.Count == 0)
                {
                    return Unauthorized(new { Message = "ユーザー名またはメールアドレスが間違っています。" });
                }
                // パスワードチェック
                string inputHashedPassword = passwordHash.HashPassword(adminUserViewModel.AdminUserPassword);
                if (inputHashedPassword != adminUserViewModelsList[0].AdminUserPassword)
                {
                    return Unauthorized(new { Message = "パスワードが間違っています。" });
                }
                AdminUserViewModel foundUser = adminUserViewModelsList[0];

                // 認証チケットの作成
                var claims = new List<Claim>
                {
                    new Claim(ClaimTypes.Name,foundUser.AdminUserName),
                    new Claim(ClaimTypes.Email,foundUser.AdminUserEmail)
                };

                var claimsIdentity = new ClaimsIdentity(
                    claims, CookieAuthenticationDefaults.AuthenticationScheme
                );

                var authProperties = new AuthenticationProperties
                {
                    // AllowRefresh = <bool>,
                    // IsPersistent = true, // 永続的なCookie(ブラウザを閉じても有効)にする場合
                    // ExpiresUtc = DateTimeOffset.UtcNow.AddMinutes(30), // 明示的な有効期限を設定
                    RedirectUri = Url.Action("Dashboard", "AdminUsers")// 認証後のリダイレクト先
                };

                // 認証Cookieを発行
                await HttpContext.SignInAsync(
                    CookieAuthenticationDefaults.AuthenticationScheme,
                    new ClaimsPrincipal(claimsIdentity),
                    authProperties
                );
                // セッションにデータを保存(例: ユーザーIDなど)
                // ASP.NET Coreのセッションは、Session Cookieを通じてサーバー側の状態を管理します。
                // 認証に成功したユーザーの識別情報などをセッションに保存することで、
                // 後続のリクエストでそのユーザーの状態を追跡できます。
                HttpContext.Session.SetString("AdminUserName", foundUser.AdminUserName);
                HttpContext.Session.SetString("AdminUserEmail", foundUser.AdminUserEmail);
                _logger.LogInformation($"ユーザー {foundUser.AdminUserName} がログインしました。");
                // ログイン成功後、Dashboardにリダイレクト
                return RedirectToAction("Dashboard", "AdminUsers");
            }
        }

    }

    [HttpGet]
    public IActionResult Dashboard()
    {
        // 認証されているかどうかを確認
        if (!User.Identity.IsAuthenticated)
        {
            // 認証されていない場合はログインページへリダイレクト
            return RedirectToAction("Index", "AdminUsers");
        }
        // セッションからユーザー情報を取得して表示することも可能
        ViewBag.AdminUserName = HttpContext.Session.GetString("AdminUserName");
        ViewBag.AdminUserEmail = HttpContext.Session.GetString("AdminUserEmail");
        return View();
    }

    [HttpGet]
    public async Task<IActionResult> Logout()
    {
        // 認証Cookieを削除し、ログアウト
        await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
        // セッションをクリア
        HttpContext.Session.Clear();
        _logger.LogInformation("ユーザーがログアウトしました。");
        // ログアウト後、ログインページにリダイレクト
        return RedirectToAction("Index", "AdminUsers");
    }

    [HttpGet]
    public IActionResult AccessDenied()
    {
        return View();
    }

    [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
    public IActionResult Error()
    {
        return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
    }
}

AdminUserLogin アクションで認証処理を行い、ログイン成功時にCookieSessionを生成します。その後、Dashboardにリダイレクトします。

解説:

using System.Security.Claims; using Microsoft.AspNetCore.Authentication;、using Microsoft.AspNetCore.Authentication.Cookies;using Microsoft.AspNetCore.Http; の追加: 認証とセッションに必要な名前空間です。

AdminUserLogin アクション内の認証処理:

ユーザー名とメールアドレス、パスワードの検証に成功したら、ClaimsPrincipal を作成します。ClaimsPrincipal は、認証されたユーザーのIDと、そのユーザーに関連付けられたクレーム(ユーザー名、メールアドレス、ロールなど)を表します。

HttpContext.SignInAsync() を呼び出して、認証Cookieを発行します。これにより、ユーザーはログイン状態になります。

authProperties.RedirectUri を設定することで、ログイン後に自動的に指定されたURIにリダイレクトさせることができます。

セッション管理:

HttpContext.Session.SetString() を使用して、サーバーサイドのセッションにユーザー情報を保存します。これにより、後続のリクエストでユーザーの特定のデータを保持できます。

Dashboard アクション:

[Authorize] アトリビュートを追加して、このアクションが認証済みのユーザーのみアクセスできるようにします。ASP.NET Coreの認可ミドルウェア (app.UseAuthorization()) がこのアトリビュートを解釈し、認証されていないユーザーのアクセスを拒否します。

User.Identity.IsAuthenticated を使用して、ユーザーが認証されているかプログラム的に確認することもできます。

HttpContext.Session.GetString() を使用して、セッションに保存されたデータを取得し、ViewBagなどを通じてビューに渡すことができます。

Logout アクションの追加:

HttpContext.SignOutAsync() を呼び出すことで、認証Cookieを削除し、ユーザーをログアウトさせます。

HttpContext.Session.Clear() を呼び出すことで、サーバーサイドのセッションデータもクリアします。

ログアウト後、ログインページにリダイレクトします。

Index アクションの改善: 既にログインしている場合はDashboardにリダイレクトするようにしました。

エラーハンドリングの改善: データベースエラーやログイン失敗時のメッセージを ModelState に追加し、ビューに渡すように変更しました。これにより、ユーザーはなぜログインできなかったのかをより明確に知ることができます。Json 形式で返していた部分は、認証フローでは通常、ログインビューを再表示し、エラーメッセージを表示することが一般的です。

サイト

1
1
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
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?