情報応用演習Ⅰ(2024)

【T8b】ASP.NET Core Identity の基礎(5/9)

プロジェクトタイプ(注意: 本文参照)
プロジェクト名T8b
ソリューション名PIT8
注意
  • 本ページの作業内容は 前のページまでの続き になっていることに注意せよ.
    • 先に前のページまでをすべて読み,指示されている作業を済ませてから本ページを読むこと.
    • プロジェクトの作成作業については準備作業を参照せよ.

8b-5. ユーザー情報作成のためのデータシーディング

ASP.NET Core Identity における「ユーザー」の情報は,アカウント関係の機能を持った専用のページ,例えばサインアップ用のページなどを通じて作成されることを想定している. したがってあらかじめ何らかのユーザーを用意しておくには データシーディング に類する処理が必要である. ただし前述したように ASP.NET Core Identity では,ユーザーの作成や削除といった処理はそのための管理クラス (UserManagerクラス)を 通じて行うので,チュートリアル【T7b】で使った OnModelCreating()メソッドを用いたデータシーディングは適用できない1OnModelCreating()メソッドでデータシーディングを行うためには,コンストラクタなどを使用して直接エンティティを生成する必要があるためである.

ASP.NET Core Identity で初期ユーザーのようなデータをあらかじめ作成する方法はいくつかありうるが,ここでは ASP.NET Core アプリケーションの初期化の仕組みに 組み込む方法を紹介する.

ここまで見てきた通り ASP.NET Core では,アプリケーションの初期化方法は Program.cs の処理によって定義される. ASP.NET Core では,アプリケーション内で使用するリソース(≒なにかのクラスのインスタンス)の初期化プロセスのために,Dependency Injection(でぺんでんしー・いんじぇくしょん)(以下,DIと表記する)と呼ばれる仕組みを採用しているが, Program.cs で定義している初期化プロセスに 初期ユーザー作成のためのクラス を差し込むことで, ユーザー情報のデータシーディングが可能になる.まず,このためのクラスを用意しよう.プロジェクト内の Data フォルダを右クリックして「追加」→「クラス」をクリックする. クラス名は何でもよいが,ここではIdentityDataSeeder(.csは省略可能)と入力して「追加」ボタンをクリックしよう. すると空のクラス定義が作られるので_の定義を書き込もう.

初期ユーザー作成のためのクラス
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
using Microsoft.AspNetCore.Identity;      // 追記

namespace T8b.Data
{
    // 初期ユーザー作成のためのクラス
    public class IdentityDataSeeder
    {
        public const string AdminRoleName = "administrators"; // アプリケーション内で用いる管理者グループの名前(決め打ち)
        public const string AdminUserName = "admin";          // アプリケーション内で用いる管理者ユーザーの名前(決め打ち)

        private readonly UserManager<IdentityUser> _userManager; // ユーザーマネージャー(ユーザーの管理クラス)
        private readonly RoleManager<IdentityRole> _roleManager; // ロールマネージャー(グループの管理クラス)

        // コンストラクタ
        public IdentityDataSeeder(UserManager<IdentityUser> userManager, 
                                  RoleManager<IdentityRole> roleManager) 
        {
            _userManager = userManager;
            _roleManager = roleManager;
        }

        // 初期ユーザー作成のためのメソッド(非同期メソッド版)
        public async Task EnsureDefaultCredentialsAsync() 
        {
            if (!await _roleManager.RoleExistsAsync(AdminRoleName))                 // 管理者グループがまだ存在していなければ
                await _roleManager.CreateAsync(new IdentityRole(AdminRoleName));    // それを作成する.

            var user = await _userManager.FindByNameAsync(AdminUserName);           // 管理者ユーザーがまだ存在していなければ
            if (user == null)                                                       // それを作成する.
            {
                user = new IdentityUser()                                           // 
                {                                                                   // ↑の定数定義した名前で管理者ユーザーオブジェクトを
                    UserName = AdminUserName,                                       // 作成して...
                    SecurityStamp = Guid.NewGuid().ToString("D")                    //
                };                                                                  //

                await _userManager.CreateAsync(user, "p@55W0rD");                   // パスワード固定でユーザーを登録し,
                await _userManager.AddToRoleAsync(user, AdminRoleName);             // 管理者グループに所属させる.
            }
        }

        // 初期ユーザー作成のためのメソッド(同期メソッド版)
        public void EnsureDefaultCredentials()
        {
            EnsureDefaultCredentialsAsync().Wait();
        }

        // Program.cs から呼び出して認証情報を作成するための静的メソッド
        public static void SeedData(IHost app) 
        {
            var factory = app.Services.GetService<IServiceScopeFactory>();            

            using (var scope = factory!.CreateScope()) 
            {
                var dataSeeder = scope.ServiceProvider.GetService<IdentityDataSeeder>();
                dataSeeder!.EnsureDefaultCredentials();
            }
        }
    }
}

_EnsureDefaultCredentialsAsync()メソッドが初期ユーザー作成の処理を行うメソッドである. このメソッドではまず25~26行目で"administrators"という名前2のグループ(≒ロール)の存在をチェックし, 存在しなければ作成している.つぎに28行目で"admin"という名前3のユーザーを検索し, 存在しなければ31~38行目でそのユーザーを作成している.このアプリケーションではこの"admin"という名前のユーザーを管理者ユーザーとして使用し, このユーザーでログインしている場合にのみ「学生」や「サークル」などの情報を登録できるようにすることにする.

なお"admin"ユーザーのパスワードは,37行目を見ると分かるように決め打ちで"p@55W0rD"にしている. このような方法は必ずしも適切ではない が,ここでは簡単のためにこのような方法をとっている. 実用的には何らかの方法で,アプリケーションを設置する者が管理者ユーザーの初期パスワードを指定できるようにしたり, またパスワードの変更ページを設けるなどして初期パスワードの変更を強制するといった対応をするべきである.

さて初期ユーザー作成のためのクラスは作成したが,まだクラスを定義しただけなので当然のことながらこれだけでは動作しない. このクラスを用いた初期化処理を行うためには Program.cs に数行書き加える必要がある. Program.cs を _に示すように変更しよう.

Program.csの変更内容2
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
using KnzwTech.AspNetCore.ResourceBasedLocalization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using T8b.Data;

AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true);

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddControllersWithViews(opt => opt.EnableDefaultErrorMessagesFromResource());

builder.Services.AddDbContext<T8bContext>(opt
    => opt.UseNpgsql(builder.Configuration.GetConnectionString(nameof(T8bContext))));

builder.Services.AddDbContext<T8bIdentityContext>(
    opt => opt.UseNpgsql(builder.Configuration.GetConnectionString(nameof(T8bIdentityContext))));

builder.Services.AddIdentity<IdentityUser, IdentityRole>()
    .AddRoleManager<RoleManager<IdentityRole>>()
    .AddDefaultTokenProviders()
    .AddEntityFrameworkStores<T8bIdentityContext>();

builder.Services.ConfigureApplicationCookie(opt => {
    opt.LoginPath = "/Admin/Login";
    opt.LogoutPath = "/Admin/Logout";
    opt.AccessDeniedPath = "/Admin/AccessDenied";
});

// 初期化プロセスに IdentityDataSeeder クラスのインスタンスの生成を登録する
builder.Services.AddTransient<IdentityDataSeeder>();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();

app.UseStatusCodePagesWithReExecute("/Home/AccessError/{0}");

app.UseRouting();

app.UseAuthentication();
app.UseAuthorization();

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

IdentityDataSeeder.SeedData(app); // 初期ユーザーの作成を実行する.

app.Run();

31行目でConfigureService()メソッド内で,先ほど定義した初期ユーザー作成のためのクラスIdentityDataSeederクラスの生成を 初期化プロセスに登録している.51行目ではIdentityDataSeederクラスの静的メソッドSeedData()メソッドを呼び出して 初期ユーザーを作成している.

ここまで書けたらIdentityDataSeederクラスのEnsureDefaultCredentialsAsync()メソッド(の始まりの{)にブレークポイントを仕掛けて 実行してみよう(_).プログラム起動直後にEnsureDefaultCredentialsAsync()メソッドが実行されていることが分かるはずである. そのままステップオーバー(F10キー)をしてメソッドの最後までどのようなパスで実行されているかを確認しておこう(_). EnsureDefaultCredentialsAsync()メソッドの処理が完了するところまでトレースしたら「続行」ボタンをクリックするか F5キーを押してアプリケーションを続行させよう. 初期化処理が終わるとそのままアプリケーションが起動してHomeコントローラーのIndexアクションが表示されるはずである(_). ここまで実行したら pgAdmin でデータベースの変化も確認しておこう. pgAdmin で「 Servers 」→「 PostgreSQL 16 」 →「 Databases 」→「 t8b_db 」を右クリックして「 Query Tool 」を起動しよう.そして, AspNetUsersテーブルの全内容を表示するSQL文を実行してadminという名前のユーザーが作成されていることを確認しておこう(_).

実行結果

ここまで確認したら,Visual Studio に戻ってデバッグ実行を終了させてから, もう一度実行してみよう (ブレークポイントは設置したままで実行すること). 先ほどと同じくEnsureDefaultCredentialsAsync()メソッドで実行が 一時停止するはずであるが,先ほど(_のとき)とは異なり 今回はグループやユーザーは作成済み になっているので if文の処理がスキップされることが分かるはずである(_).ここまで確認できたらプログラムを終了してブレークポイントを解除しておこう.

いま確認した通りこのアプローチでは, 管理者ユーザー/グループの存在確認をアプリケーションが起動するたびに毎回行うことになり無駄が多い

通常このようなユーザーデータのデータシーディングの処理はアプリケーションの設置の際に一度だけ実行すればよいので, アプリケーションのインストール時や初回起動時,あるいはユーザーが明示的に指定したタイミング─例えばなんらかのコントローラーのアクションに アクセスしたタイミングや,あるいはアプリケーションに特定の起動フラグが与えられた場合にのみ行う,といったアプローチのほうが より合理的である.

こういった挙動の設計はアプリケーションごとに異なるので,実用に供されるウェブアプリを作成する際は熟考することを推奨する.


  1. できなくもないが推奨されない ↩︎

  2. AdminRoleNameという定数に保存されている ↩︎

  3. AdminUserNameという定数に保存されている ↩︎

Last updated on 2024-06-10
Published on 2024-06-10

Powered by Hugo. Theme by TechDoc. Designed by Thingsym.