0
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

【C#入門】初学者がASP.NETでWebアプリを作る:第5回

Posted at

今回やること

アカウント認証の実装の続きです。

前回足りてない修正

前回の状態で一度起動すると、エラーになってしまい起動できませんでした。
そこで下記2つの修正を行いました。

①Startup.csのConfigureServicesにAddRazorPagesを追加

Startup.cs
// This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<ApplicationDbContext>(options =>
                options.UseNpgsql(
                    Configuration.GetConnectionString("DefaultConnection")));
            services.AddDatabaseDeveloperPageExceptionFilter();

            //services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
            //    .AddEntityFrameworkStores<ApplicationDbContext>();
            services.AddIdentity<SMSUser, IdentityRole>()
                .AddEntityFrameworkStores<ApplicationDbContext>()
                .AddDefaultTokenProviders();
            services.AddControllersWithViews();
            services.AddRazorPages(); // 追加部分
        }

②_LoginPartial.cshtmlを修正

_LoginPartial.cshtml
@using Microsoft.AspNetCore.Identity
@inject SignInManager<SMSUser> SignInManager  ←ここの型引数を独自ユーザクラスにする
@inject UserManager<SMSUser> UserManager    ←ここの型引数を独自ユーザクラスにする

<ul class="navbar-nav">
@if (SignInManager.IsSignedIn(User))
{
    <li class="nav-item">
        <a  class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Manage/Index" title="Manage">ようこそ @User.Identity.Name!</a>
    </li>
    <li class="nav-item">
        <form  class="form-inline" asp-area="Identity" asp-page="/Account/Logout" asp-route-returnUrl="@Url.Action("Index", "Home", new { area = "" })">
            <button  type="submit" class="nav-link btn btn-link text-dark">ログアウト</button>
        </form>
    </li>
}
else
{
    <li class="nav-item">
        <a class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Register">アカウント登録</a>
    </li>
    <li class="nav-item">
        <a class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Login">ログイン</a>
    </li>
}
</ul>

起動するとこのような画面になります。アカウント登録してみます。
image.png

URLは変わったんですが何も画面遷移しませんでした。
対応するビューとかコントローラーがないからですね。
デフォルトだとASP.NET Core Identityのライブラリにそのあたりが含まれているようで、
独自クラスを使ったとたんそれらが使えなくなったってことですね…
image.png

で、ライブラリに含まれているコントローラやらビューやらはスキャフォールディングで作れる機能があります。
プロジェクトを右クリックして、追加→新規スキャフォールディングアイテムでIDを選び、追加ボタンを押します。
image.png

自分に合った設定をします。オーバーライドはよくわからないので全部にしておきます。
(おそらくオーバーライドしたいもの(編集したい画面とか機能)をチェックすると、実体ができるのだと思いました。)
image.png

はいエラー。どうやらDbContextはIdentityDbContextを継承しないといけないようです。
image.png

なので前回の修正を戻します。

ApplicationDbContext.cs
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Text;

namespace SalaryManagementSystem.Data
{
    public class ApplicationDbContext : IdentityDbContext
    //public class ApplicationDbContext : DbContext
    {
        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
            : base(options)
        {
        }

        public DbSet<SalaryManagementSystem.Models.Salary> Salary { get; set; }
        public DbSet<SalaryManagementSystem.Models.SMSUser> SMSUsers { get; set; }
    }
}

もう一度やってみます。ユーザークラスが選べねぇ。
image.png

謎のテキストファイルが表示されました。うまくいったようです?
image.png

ユーザクラスが選べなかった(デフォルト:IdentityUserから変更できなかった)のは、DbContextの書き方がおかしかったようです。

ApplicationDbContext.cs
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using SalaryManagementSystem.Models;
using System;
using System.Collections.Generic;
using System.Text;

namespace SalaryManagementSystem.Data
{
    public class ApplicationDbContext : IdentityDbContext <SMSUser>  //←ここに型引数が必要
    //public class ApplicationDbContext : DbContext
    {
        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
            : base(options)
        {
        }

        public DbSet<SalaryManagementSystem.Models.Salary> Salary { get; set; }
        public DbSet<SalaryManagementSystem.Models.SMSUser> SMSUsers { get; set; }
    }
}

もう一度スキャフォールディング…ユーザクラスは選べない。
追加を押すと、既にありますと言われますがOk。
image.png

さらにStartup.csを修正します。

Startup.cs
        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<ApplicationDbContext>(options =>
                options.UseNpgsql(
                    Configuration.GetConnectionString("DefaultConnection")));
            services.AddDatabaseDeveloperPageExceptionFilter();

            //services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
            //    .AddEntityFrameworkStores<ApplicationDbContext>();
            services.AddIdentity<SMSUser, IdentityRole>()
                .AddEntityFrameworkStores<ApplicationDbContext>()
                .AddDefaultTokenProviders()
                .AddDefaultUI();        // ←追加
            services.AddControllersWithViews();
            services.AddRazorPages();
        }

登録画面には遷移するようになりました。ただ、入力項目等があるべき形になっていないので、修正する必要があります。
image.png

登録画面の修正

登録画面のロジックを修正していきます。
まずはコードビハインド。
(そもそもASP.NETはコードビハインドって言わない?WPFだけの話なのかな…)

Register.cshtml.cs
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Logging;
using SalaryManagementSystem.Models;

namespace SalaryManagementSystem.Areas.Identity.Pages.Account
{
    [AllowAnonymous]
    public class RegisterModel : PageModel
    {
        private readonly SignInManager<SMSUser> _signInManager;
        private readonly UserManager<SMSUser> _userManager;
        private readonly ILogger<RegisterModel> _logger;
        //private readonly IEmailSender _emailSender;

        public RegisterModel(
            UserManager<SMSUser> userManager,
            SignInManager<SMSUser> signInManager,
            ILogger<RegisterModel> logger
            //IEmailSender emailSender)
            )
        {
            _userManager = userManager;
            _signInManager = signInManager;
            _logger = logger;
            //_emailSender = emailSender;
        }

        [BindProperty]
        public InputModel Input { get; set; }

        public string ReturnUrl { get; set; }

        public IList<AuthenticationScheme> ExternalLogins { get; set; }

        public class InputModel
        {
            //[Required]
            //[EmailAddress]
            //[Display(Name = "Email")]
            //public string Email { get; set; }

            [Required]
            [StringLength(100, ErrorMessage = "{0} は {2} ~ {1} 文字で入力してください。", MinimumLength = 6)]
            [Display(Name = "ユーザID")]
            public string UserId { get; set; }

            [Required]
            [StringLength(100, ErrorMessage = "{0} は {2} ~ {1} 文字で入力してください。", MinimumLength = 6)]
            [Display(Name = "ユーザ名")]
            public string UserName { get; set; }

            [Required]
            [StringLength(100, ErrorMessage = "{0} は {2} ~ {1} 文字で入力してください。", MinimumLength = 6)]
            [DataType(DataType.Password)]
            [Display(Name = "パスワード")]
            public string Password { get; set; }

            [DataType(DataType.Password)]
            [Display(Name = "パスワード再入力")]
            [Compare("Password", ErrorMessage = "パスワードが一致しません。")]
            public string ConfirmPassword { get; set; }
        }

        public async Task OnGetAsync(string returnUrl = null)
        {
            ReturnUrl = returnUrl;
            ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
        }

        public async Task<IActionResult> OnPostAsync(string returnUrl = null)
        {
            returnUrl ??= Url.Content("~/");
            ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
            if (ModelState.IsValid)
            {
                //var user = new SMSUser { UserName = Input.Email, Email = Input.Email };
                var user = new SMSUser { SMSUserId = Input.UserId, SMSUserName = Input.UserName, IsLocked = false, LoginFailCount = 0, CreateDate = DateTime.Now, UpdateDate = DateTime.Now };
                var result = await _userManager.CreateAsync(user, Input.Password);
                if (result.Succeeded)
                {
                    _logger.LogInformation("User created a new account with password.");

                    //var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
                    //code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
                    //var callbackUrl = Url.Page(
                    //    "/Account/ConfirmEmail",
                    //    pageHandler: null,
                    //    values: new { area = "Identity", userId = user.Id, code = code, returnUrl = returnUrl },
                    //    protocol: Request.Scheme);

                    //await _emailSender.SendEmailAsync(Input.Email, "Confirm your email",
                    //    $"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");

                    //if (_userManager.Options.SignIn.RequireConfirmedAccount)
                    //{
                    //    return RedirectToPage("RegisterConfirmation", new { email = Input.Email, returnUrl = returnUrl });
                    //}
                    //else
                    //{
                        await _signInManager.SignInAsync(user, isPersistent: false);
                        return LocalRedirect(returnUrl);
                    //}
                }
                foreach (var error in result.Errors)
                {
                    ModelState.AddModelError(string.Empty, error.Description);
                }
            }

            // If we got this far, something failed, redisplay form
            return Page();
        }
    }
}

次にビュー。

Register.cshtml
@page
@model RegisterModel
@{
    ViewData["Title"] = "アカウント登録";
}

<h1>@ViewData["Title"]</h1>

<div class="row">
    <div class="col-md-4">
        <form asp-route-returnUrl="@Model.ReturnUrl" method="post">
            <!--<h4>Create a new account.</h4>-->
            <hr />
            <div asp-validation-summary="All" class="text-danger"></div>
            <!--<div class="form-group">
        <label asp-for="Input.Email"></label>
        <input asp-for="Input.Email" class="form-control" />
        <span asp-validation-for="Input.Email" class="text-danger"></span>
    </div>-->
            <div class="form-group">
                <label asp-for="Input.UserId"></label>
                <input asp-for="Input.UserId" class="form-control" />
                <span asp-validation-for="Input.UserId" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Input.UserName"></label>
                <input asp-for="Input.UserName" class="form-control" />
                <span asp-validation-for="Input.UserName" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Input.Password"></label>
                <input asp-for="Input.Password" class="form-control" />
                <span asp-validation-for="Input.Password" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Input.ConfirmPassword"></label>
                <input asp-for="Input.ConfirmPassword" class="form-control" />
                <span asp-validation-for="Input.ConfirmPassword" class="text-danger"></span>
            </div>
            <button type="submit" class="btn btn-primary">Register</button>
        </form>
    </div>
    <div class="col-md-6 col-md-offset-2">
        <section>
            <!--<h4>Use another service to register.</h4>
    <hr />
    @{
        if ((Model.ExternalLogins?.Count ?? 0) == 0)
        {
            <div>
                <p>
                    There are no external authentication services configured. See <a href="https://go.microsoft.com/fwlink/?LinkID=532715">this article</a>
                    for details on setting up this ASP.NET application to support logging in via external services.
                </p>
            </div>
        }
        else
        {
            <form id="external-account" asp-page="./ExternalLogin" asp-route-returnUrl="@Model.ReturnUrl" method="post" class="form-horizontal">
                <div>
                    <p>
                        @foreach (var provider in Model.ExternalLogins)
                        {
                            <button type="submit" class="btn btn-primary" name="provider" value="@provider.Name" title="Log in using your @provider.DisplayName account">@provider.DisplayName</button>
                        }
                    </p>
                </div>
            </form>
        }
    }-->
            <h4>各項目は必須項目です。</h4>
            <p>・ユーザIDはログイン時に使用します。6~10文字で入力してください。</p>
            <p>・ユーザ名は画面に表示されます。1~100文字で入力してください。</p>
            <p>・パスワードは6~100文字で入力してください。</p>
        </section>
    </div>
</div>

@section Scripts {
    <partial name="_ValidationScriptsPartial" />
}

ほかにちょっとだけ直すところがあります。
_ManageNav.cshtml内で自作ユーザクラスを使っていますが、usingがないのでエラーになってしまいます。
なので、usingを追加しておきます。

_ManageNav.cshtml
@using SalaryManagementSystem.Models;  ←追加
@inject SignInManager<SMSUser> SignInManager

下記のような画面になります。
image.png

適当に入力してみます。
image.png

Usernameが入ってないといわれました。
image.png

Register.cshtml.csをちょっと修正。不本意ながら使わないUserNameを設定。

Register.cshtml.cs
//var user = new SMSUser { UserName = Input.Email, Email = Input.Email };
var user = new SMSUser { SMSUserId = Input.UserId, SMSUserName = Input.UserName, UserName = Input.UserName, IsLocked = false, LoginFailCount = 0, CreateDate = DateTime.Now, UpdateDate = DateTime.Now };

どうやら日本語はだめらしい。こちらを参考に日本語対応する。
image.png

Startup.cs
            //services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
            //    .AddEntityFrameworkStores<ApplicationDbContext>();
            services.AddIdentity<SMSUser, IdentityRole>(options =>
            {
                options.User.AllowedUserNameCharacters = null;
            })
                .AddEntityFrameworkStores<ApplicationDbContext>()
                .AddDefaultTokenProviders()
                .AddDefaultUI();

登録を押すとエラー。DBを更新せよと。
image.png

とりあえず指示通りAdd-MigrationとUpdate-Databaseする。
Add-Migrationで警告が出るがまあなんでもいいので無視。

PM> Add-Migration Init_20210202
Build started...
Build succeeded.
An operation was scaffolded that may result in the loss of data. Please review the migration for accuracy.
To undo this action, use Remove-Migration.
PM> Update-Database
Build started...
Build succeeded.
Done.

登録できました。右上にユーザー名も表示されました。
image.png

DBを見ると、SMSUserテーブルではなく、AspNetUsersに登録されていました。ナンデ!?
image.png

ちなみに独自項目はAspNetUsersテーブルにできていました。UserName1ってなに…
image.png

次回予告

次回はログインします。

0
5
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
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?