Cookie 認証 を使用してログインの仕組みを作り、認証していない場合リダイレクトされる仕組みを作る

Siden oppdatert :
ページ作成日 :

動作確認環境

Visual Studio
  • Visual Studio Community 2022
ASP.NET Core (MVC, Razor Pages)
6.0

はじめに

今回 ASP.NET Core でログインによる認証の仕組みとして Cookie 認証を使用します。 Cookie 認証は従来のフォーム認証と同じようなものだと思ってもらって構いません。

ASP.NET Core の認証の仕組みとしては他に ASP.NET Core Identity があります。 こちらはフォームを使用した認証のほかに API の認証、外部のログインサービスを利用できたりパスワードの管理やリセットなど 沢山の機能を利用することができます。しかし、今回単純なログイン画面を作るだけという観点から見るとやや大げさな認証の仕組みになるので 今回は使用しません。

今回紹介する Cookie 認証の Tips では、ログインしない限りはログイン画面以外は表示することができず、 別の画面に移動しようとするとログイン画面にリダイレクトされます。 ログインすれば他の画面も表示することができます。

一応ログイン画面でユーザー名とパスワードを入力すればログインできるようになっていますが、 ユーザーの認証自体は仮置きで実装しています。 今回はあくまでも Cookie 認証の実装をメインとしていますので、パスワードが正しいかなどの判定処理については本質とはしていません。

この Tips ではプログラムは部分的に記載しています。完全なコードについてはプログラム一式をダウンロードして下さい。 また、MVC, Razor Pages 両方のフレームワークについて掲載しています。

プロジェクトの作成

Visual Studio を起動し新しいプロジェクトを作成します。

Razor Pages の場合は「ASP.NET Core Web アプリ」を、MVC の場合は「ASP.NET Core Web アプリ (Model-View-Controller)」を選択します。

任意のプロジェクト名とプロジェクトの場所を指定します。

認証の種類については「なし」を選択してください。他の認証を選択した場合、ASP.NET Core Identity を使用することとなります。設定が終わったら「作成」ボタンをクリックします。

プロジェクトを作成後、デバッグ実行すると下図の画面が表示されます。この画面をベースにプログラムを作成していきます。

Program.cs の編集 (Razor Pages, MVC 共通)

Program.cs に Cookie 認証に必要な定義を追加していきます。 追加する名前空間は以下のものになります。

using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;

以下は builder.Services に追加するコードです。

// === 省略 ===

// 「Razor Pages」のコード
// builder.Services.AddRazorPages();
// 「MVC」のコード
// builder.Services.AddControllersWithViews();

// ※ここから追加

// Cookie による認証スキームを追加する
builder.Services
  .AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
  .AddCookie();

builder.Services.AddAuthorization(options =>
{
  // AllowAnonymous 属性が指定されていないすべての画面、アクションなどに対してユーザー認証が必要となる
  options.FallbackPolicy = new AuthorizationPolicyBuilder()
    .RequireAuthenticatedUser()
    .Build();
});

// ※ここまで追加

var app = builder.Build();

// === 省略 ===

AddAuthentication メソッドと AddCookie メソッドを実行することによって Cookie 認証を有効にできます。 スキーム名は特に変更する必要がない場合は CookieAuthenticationDefaults.AuthenticationScheme を指定しておきます。

AddAuthorization メソッドの options.FallbackPolicyRequireAuthenticatedUser を指定すると 全ページ、全コントローラー、アクションに認証必須のポリシーを適用させることができます。 ログイン画面以外を認証必須にする場合はコードの削減、記載ミスを防ぐ意味でも有用な方法です。 ログイン画面だけ個別に認証不要のコードを記述することになります。

以下は app に対するコードです。

// === 省略 ===

app.UseRouting();

app.UseAuthentication(); // [追加] 認証
app.UseAuthorization(); // 認可

// 「Razor Pages」のコード
// app.MapRazorPages();
// 「MVC」のコード
// app.MapControllerRoute(
//     name: "default",
//     pattern: "{controller=Home}/{action=Index}/{id?}");

// === 省略 ===

アプリケーションに認証機能を追加するので app.UseAuthentication() を追加します。 記述場所は MSDN のドキュメントにしたがって app.UseAuthorization() の前に配置します。 それ以外はテンプレートのままです。

Razor Pages プロジェクトの場合のプログラム

ログインページの作成 (Pages/Account/Login.cshtml.cs)

ファイルの作成

ログインページを作成します。ファイルパスは「/Pages/Account/Login.cshtml」として作成する必要があります。 これはデフォルトのログインパスがそのようになっているためです。 このパスを変えたい場合は Program.csAddCookie メソッドの引数を設定することによって変えることができます。

メニューから作成せず他のファイルをコピーして作成しても構いませんが、その場合はプログラムを正しく直してください。

ログインしなくてもログインページにアクセスできるようにする

Program.cs の設定で全てのページはログインしている状態でないとアクセスできないように設定しているのでログインページのみはログインしていなくてもアクセスできるように設定する必要があります。

AllowAnonymous 属性を付与することにより、対象ページは認証されていなくてもアクセスすることができるようになります。 AllowAnonymous 属性はログイン画面以外のほかの利用場所としては Cookie 認証と無関係な API 処理などでも使用することがあります。

using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Security.Claims;

namespace AspNetCoreCookieAuthenticationRazorPages.Pages.Account
{
  [AllowAnonymous]
  public class LoginModel : PageModel
  {
  }
}

入力を受け取る変数を作成

ログインではユーザー名をパスワードを入力するのでそれらの値を受け取れるように宣言します。

// 省略

[AllowAnonymous]
public class LoginModel : PageModel
{
  /// <summary>ユーザー名。</summary>
  [BindProperty]
  [Required]
  [DisplayName("ユーザー名")]
  public string UserName { get; set; } = "";

  /// <summary>パスワード。</summary>
  [BindProperty]
  [Required]
  [DataType(DataType.Password)]
  [DisplayName("パスワード")]
  public string Password { get; set; } = "";
}

ログイン認証用のユーザー名とパスワードを定義

本来はデータベースなどに保存するのですが、今回ユーザー判定は主眼ではないので仮置きで作っておきます。

// 省略

[AllowAnonymous]
public class LoginModel : PageModel
{
  // 省略

  /// <summary>仮のユーザーデータベースとする。</summary>
  private Dictionary<string, string> UserAccounts { get; set; } = new Dictionary<string, string>
    {
      { "user1", "password1" },
      { "user2", "password2" },
    };
}

ログイン処理

/// <summary>ログイン処理。</summary>
public async Task<ActionResult> OnPost()
{
  // 入力内容にエラーがある場合は処理を中断してエラー表示
  if (ModelState.IsValid == false) return Page();

  // ユーザーの存在チェックとパスワードチェック (仮実装)
  // 本 Tips は Cookie 認証ができるかどうかの確認であるため入力内容やパスワードの厳密なチェックは行っていません
  if (UserAccounts.TryGetValue(UserName, out string? getPass) == false || Password != getPass)
  {
    ModelState.AddModelError("", "ユーザー名またはパスワードが一致しません。");
    return Page();
  }

  // サインインに必要なプリンシパルを作る
  var claims = new[] { new Claim(ClaimTypes.Name, UserName) };
  var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
  var principal = new ClaimsPrincipal(identity);

  // 認証クッキーをレスポンスに追加
  await HttpContext.SignInAsync(principal);

  // ログインが必要な画面にリダイレクトします
  return RedirectToPage("/Index");
}

ログインボタンを押した後の認証処理です。ユーザー名とパスワードが一致していれば認証可能としています。

記載しているコードは認証に必要な最低限のコードであり、Claim, ClaimsIdentity, ClaimsPrincipal を定義し、 HttpContext.SignInAsync メソッドを呼ぶことによって Cookie が生成され、認証されたことになります。

クレームが追加で必要であったり、Cookie の有効期限が必要であればパラメータを追加していきます。

ログイン後、認証が必須である /Index にリダイレクトさせています。

ログアウト処理

/// <summary>ログアウト処理。</summary>
public async Task OnGetLogout()
{
  // 認証クッキーをレスポンスから削除
  await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
}

Logout ハンドラー付きでアクセスしたときはログアウトするように処理します。 HttpContext.SignOutAsync メソッドを呼び出すことによって Cookie を削除しログインしていない状態に戻すことができます。

ビューの作成

見た目は特に考慮していません。下図のようなユーザー名とパスワードを入力する欄を追加しログインするためのボタンを配置します。 テスト用にログインせずに /Index にアクセスするリンクも配置しておきます。

ユーザー名とパスワードの入力が面倒なので初期値を設定しています。

@page
@model AspNetCoreCookieAuthenticationRazorPages.Pages.Account.LoginModel
@{}

<form asp-action="Login">
  <div class="row m-1 g-3">
    <div class="col-sm-6 offset-sm-3">
      <div asp-validation-summary="ModelOnly" class="text-danger"></div>
    </div>
  </div>

  <div class="row m-1 g-3">
    <div class="col-sm-6 offset-sm-3">
      <label asp-for="UserName" class="form-label"></label>
      <input asp-for="UserName" class="form-control" value="user1" />
      <span asp-validation-for="UserName" class="text-danger"></span>
    </div>
  </div>
  <div class="row m-1 g-3">
    <div class="col-sm-6 offset-sm-3">
      <label asp-for="Password" class="form-label"></label>
      <input asp-for="Password" class="form-control" value="password1" />
      <span asp-validation-for="Password" class="text-danger"></span>
    </div>
  </div>
  <div class="row m-1 g-3">
    <div class="col-sm-6 offset-sm-3">
      <button type="submit" class="btn btn-primary">ログイン</button>
    </div>
  </div>
  <div class="row m-1 g-3">
    <div class="col-sm-6 offset-sm-3">
      <a asp-page="/Index">認証が必要な画面へ直接リンク</a>
    </div>
  </div>
</form>

@section Scripts {
  @{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); }
}

ログアウトリンクの作成 (/Pages/Shared/_Layout.cshtml)

ホーム画面は特に変更はしませんが、ナビゲーションバーにログアウトのリンクを貼っておきます。 また、テスト用にログアウトせずにログイン画面に遷移するリンクも貼っておきます。

<!-- 中略 -->
<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-page="/Index">Home</a>
    </li>
    <li class="nav-item">
      <a class="nav-link text-dark" asp-area="" asp-page="/Privacy">Privacy</a>
    </li>
    <!-- ここから追加 -->
    <li class="nav-item">
      <a class="nav-link text-dark" asp-page="/Account/Login" asp-page-handler="Logout">ログアウト</a>
    </li>
    <li class="nav-item">
      <a class="nav-link text-dark" asp-page="/Account/Login">ログアウトせずログインへ</a>
    </li>
    <!-- ここまで追加 -->
  </ul>
</div>
<!-- 中略 -->

Razor Pages のコードは以上です。

MVC プロジェクトの場合のプログラム

ログインモデルの作成

ログイン画面で入力した値を受け取るためのモデルを作成しておきます。

namespace AspNetCoreCookieAuthenticationMvc.Models
{
  public class LoginModel
  {
    /// <summary>ユーザー名。</summary>
    [Required]
    [DisplayName("ユーザー名")]
    public string UserName { get; set; } = "";

    /// <summary>パスワード。</summary>
    [Required]
    [DataType(DataType.Password)]
    [DisplayName("パスワード")]
    public string Password { get; set; } = "";
  }
}

AccountController の作成

ログイン画面を作成するのに必要なコントローラーとアクションを作成します。 コントローラー名は AccountController として作成します。 これはデフォルトでログイン画面のコントローラー名とアクション名が「~/Account/Login」と決まっているためです。 このパスを変更したい場合は Program.cs の AddCookie メソッドのオプションで変更可能です。 ここではデフォルトの設定で進めていきます。

まずは Controller の側を作成します。

using AspNetCoreCookieAuthenticationMvc.Models;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Security.Claims;

namespace AspNetCoreCookieAuthenticationMvc.Controllers
{
  /// <remarks>
  /// <see cref="AllowAnonymous"/> 属性は Cookie 認証していなくてもアクセスできる Action (Controller) であることを示す。
  /// </remarks>
  [AllowAnonymous]
  public class AccountController : Controller
  {
  }
}

AllowAnonymous 属性を付与することにより、中のすべてのアクションは認証されていなくても実行できるようになります。 これによりログイン画面のみ認証無しでアクセスできるようになります。

AllowAnonymous 属性はログイン画面以外のほかの利用場所としては Cookie 認証と無関係な API 専用コントローラーでも使用することがあります。

次にログインできるユーザーとパスワードを定義します。 本来はデータベースなどに保存するのですが、今回ユーザー判定は主眼ではないので仮置きで作っておきます。

[AllowAnonymous]
public class AccountController : Controller
{
  /// <summary>仮のユーザーデータベースとする。</summary>
  private Dictionary<string, string> UserAccounts { get; set; } = new Dictionary<string, string>
    {
      { "user1", "password1" },
      { "user2", "password2" },
    };
}

以下はログイン画面を表示するアクションです。 表示するだけなのでビューをそのまま返します。

/// <summary>ログイン画面を表示します。</summary>
public IActionResult Login() => View();

以下はログイン時に処理するコードです。

/// <summary>ログイン処理を実行します。</summary>
[HttpPost]
public async Task<IActionResult> Login(LoginModel model)
{
  // 入力内容にエラーがある場合は処理を中断してエラー表示
  if (ModelState.IsValid == false) return View(model);

  // ユーザーの存在チェックとパスワードチェック (仮実装)
  // 本 Tips は Cookie 認証ができるかどうかの確認であるため入力内容やパスワードの厳密なチェックは行っていません
  if (UserAccounts.TryGetValue(model.UserName, out string? getPass) == false || model.Password != getPass)
  {
    ModelState.AddModelError("", "ユーザー名またはパスワードが一致しません。");
    return View(model);
  }

  // サインインに必要なプリンシパルを作る
  var claims = new[] { new Claim(ClaimTypes.Name, model.UserName) };
  var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
  var principal = new ClaimsPrincipal(identity);

  // 認証クッキーをレスポンスに追加
  await HttpContext.SignInAsync(principal);

  // ログインが必要な画面にリダイレクトします
  return RedirectToAction(nameof(HomeController.Index), "Home");
}

ログインボタンを押した後の認証処理です。ユーザー名とパスワードが一致していれば認証可能としています。

記載しているコードは認証に必要な最低限のコードであり、Claim, ClaimsIdentity, ClaimsPrincipal を定義し、 HttpContext.SignInAsync メソッドを呼ぶことによって Cookie が生成され、認証されたことになります。

クレームが追加で必要であったり、Cookie の有効期限が必要であればパラメータを追加していきます。

ログイン後、認証が必須である ~/Home/Index にリダイレクトさせています。

最後にログアウト用の処理を入れます。

/// <summary>ログアウト処理を実行します。</summary>
public async Task<IActionResult> Logout()
{
  // 認証クッキーをレスポンスから削除
  await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);

  // ログイン画面にリダイレクト
  return RedirectToAction(nameof(Login));
}

HttpContext.SignOutAsync メソッドを呼び出すことによって Cookie を削除しログインしていない状態に戻すことができます。

ビュー(ログインフォーム)の作成 (/Views/Account/Login.cshtml)

Login アクションを右クリックしてビューを追加します。他のファイルからコピーして作っても構いません。

見た目は特に考慮していません。下図のようなユーザー名とパスワードを入力する欄を追加しログインするためのボタンを配置します。 テスト用にログインせずに Home/Index にアクセスするリンクも配置しておきます。

ユーザー名とパスワードの入力が面倒なので初期値を設定しています。

@model LoginModel
@{}

<form asp-action="Login">
  <div class="row m-1 g-3">
    <div class="col-sm-6 offset-sm-3">
      <div asp-validation-summary="ModelOnly" class="text-danger"></div>
    </div>
  </div>

  <div class="row m-1 g-3">
    <div class="col-sm-6 offset-sm-3">
      <label asp-for="UserName" class="form-label"></label>
      <input asp-for="UserName" class="form-control" value="user1" />
      <span asp-validation-for="UserName" class="text-danger"></span>
    </div>
  </div>
  <div class="row m-1 g-3">
    <div class="col-sm-6 offset-sm-3">
      <label asp-for="Password" class="form-label"></label>
      <input asp-for="Password" class="form-control" value="password1" />
      <span asp-validation-for="Password" class="text-danger"></span>
    </div>
  </div>
  <div class="row m-1 g-3">
    <div class="col-sm-6 offset-sm-3">
      <button type="submit" class="btn btn-primary">ログイン</button>
    </div>
  </div>
  <div class="row m-1 g-3">
    <div class="col-sm-6 offset-sm-3">
      <a asp-controller="Home" asp-action="Index">認証が必要な画面へ直接リンク</a>
    </div>
  </div>
</form>

@section Scripts {
  @{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); }
}

ログアウトリンクの作成 (/Views/Shared/_Layout.cshtml)

ホーム画面は特に変更はしませんが、ナビゲーションバーにログアウトのリンクを貼っておきます。 また、テスト用にログアウトせずにログイン画面に遷移するリンクも貼っておきます。

<!-- 中略 -->
<div class="navbar-collapse collapse d-sm-inline-flex flex-sm-row-reverse">
  <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-controller="Account" asp-action="Logout">ログアウト</a>
    </li>
    <li class="nav-item">
      <a class="nav-link text-dark" asp-controller="Account" asp-action="Login">ログアウトせずログインへ</a>
    </li>
    <!-- ここまで追加 -->
  </ul>
</div>
<!-- 中略 -->

MVC のコードは以上です。

動作確認

これで Cookie 認証で最低限必要な実装は完了しました。 実行し動作を確認してみてください。 ログインしているかしていないかによって動作が変わるはずです。 簡単な例としては以下のような動作を確認できると思います。

操作 動作結果
ログインせずホームへ ログイン画面へリダイレクト
ログイン ホーム画面へ
ホームからログアウトしてログインせずホームへ ログイン画面へリダイレクト
ホームからログアウトせず、かつログインせずホームへ ホーム画面へ