Cookie 認証 を使用してログインしないとリダイレクトされる仕組みを作る (ASP.NET Core 3.0)

ページ更新日 :
ページ作成日 :

環境

Visual Studio
  • Visual Studio Community 2019
ASP.NET Core
3.0 以降

Cookie 認証は .NET Core 2.2 以前でも使用できますが、当 Tips では .NET Core 3.0 以降専用のコードで記載しています。

はじめに

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

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

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

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

この Tips ではプログラムは部分的に記載しています。完全なコードについてはプログラム一式をダウンロードして下さい。

プロジェクトの作成

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

image

ASP.NET Core Web アプリケーションを選択します。

image

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

image

今回は MVC のプロジェクトを使用しますが、他のテンプレートでも大部分は代用可能です。

認証については認証無しを選択してください。他の認証を選択した場合、ASP.NET Core Identity を使用することとなります。

image

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

image

Startup.cs の編集

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

using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

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

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

public void ConfigureServices(IServiceCollection services)
{
  services.AddControllersWithViews();
  
  // Cookie による認証スキームを追加する
  services
    .AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie();
  
  services.AddAuthorization(options =>
  {
    // AllowAnonymous 属性が指定されていないすべての Action などに対してユーザー認証が必要となる
    options.FallbackPolicy = new AuthorizationPolicyBuilder()
      .RequireAuthenticatedUser()
      .Build();
  });
}

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

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
  if (env.IsDevelopment())
  {
    app.UseDeveloperExceptionPage();
  }
  else
  {
    app.UseExceptionHandler("/Home/Error");
    // The default HSTS value is 30 days. ...
    app.UseHsts();
  }
  app.UseHttpsRedirection();
  app.UseStaticFiles();
  
  app.UseRouting();
  
  app.UseAuthentication(); // [追加] 認証
  app.UseAuthorization(); // 認可
  
  app.UseEndpoints(endpoints =>
  {
    endpoints.MapControllerRoute(
      name: "default",
      pattern: "{controller=Home}/{action=Index}/{id?}");
  });
}

AccountController の作成

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

image

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

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

using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Security.Claims;
using System.Threading.Tasks;

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

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

/// <summary>仮のユーザーデータベースとする。</summary>
private Dictionary<string, string> UserAccounts { get; set; }

public AccountController()
{
  // 仮のユーザーを登録する
  UserAccounts = new Dictionary<string, string>
  {
    { "user1", "password1" },
    { "user2", "password2" },
  };
}

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

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

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

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

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

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

/// <summary>ログイン処理を実行します。</summary>
[HttpPost]
public async Task<IActionResult> Login(string userName, string password)
{
  // ユーザーの存在チェックとパスワードチェック (仮実装)
  // 本 Tips は Cookie 認証ができるかどうかの確認であるため入力内容やパスワードの厳密なチェックは行っていません
  if (UserAccounts.TryGetValue(userName, out string getPass) == false || password != getPass)
  {
    return View();
  }

  // サインインに必要なプリンシパルを作る
  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 RedirectToAction(nameof(HomeController.Index), "Home");
}

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

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

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

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

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

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

image

@{
  Layout = null;
}
<!DOCTYPE html>
<html>
<head>
  <meta name="viewport" content="width=device-width" />
  <title>Login</title>
</head>
<body>
  <form method="post">
    <input name="userName" type="text" placeholder="ユーザー名" value="user1" />
    <input name="password" type="password" placeholder="パスワード" value="password1" />
    <button type="submit">ログイン</button>
  </form>
  <p><a asp-controller="Home" asp-action="Index">認証が必要な画面へ直接リンク</a></p>
</body>
</html>

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

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

image

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

動作確認

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

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