今回やること
・前回足りてなかったこと
・ログイン
前回足りていなかったこと
DBをよく見てみると、なんとパスワードが登録されていませんでした。
Register.cshtml.csでハッシュ化したパスワードを設定するようにしました。
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, UserName = Input.UserName, IsLocked = false, LoginFailCount = 0, CreateDate = DateTime.Now, UpdateDate = DateTime.Now };
user.Password = new PasswordHasher<SMSUser>().HashPassword(user, Input.Password); // 追加部分
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();
}
再度アカウント登録してみると、ハッシュ化されたパスワードが保存されました。
ログインする
まずログイン画面が英語なのと、メールアドレスを入力するようになっているので、修正していきます。
コードから直すのとcshtmlから直すのとではどちらが楽なんでしょう?
まずはLogincshtml.cs
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;
using SalaryManagementSystem.Models;
namespace SalaryManagementSystem.Areas.Identity.Pages.Account
{
[AllowAnonymous]
public class LoginModel : PageModel
{
private readonly UserManager<SMSUser> _userManager;
private readonly SignInManager<SMSUser> _signInManager;
private readonly ILogger<LoginModel> _logger;
public LoginModel(SignInManager<SMSUser> signInManager,
ILogger<LoginModel> logger,
UserManager<SMSUser> userManager)
{
_userManager = userManager;
_signInManager = signInManager;
_logger = logger;
}
[BindProperty]
public InputModel Input { get; set; }
public IList<AuthenticationScheme> ExternalLogins { get; set; }
public string ReturnUrl { get; set; }
[TempData]
public string ErrorMessage { get; set; }
public class InputModel
{
//[Required]
//[EmailAddress]
//public string Email { get; set; }
[Required]
[Display(Name = "ユーザID")]
public string UserId { get; set; }
[Required]
[Display(Name = "パスワード")]
[DataType(DataType.Password)]
public string Password { get; set; }
[Display(Name = "記憶する")]
public bool RememberMe { get; set; }
}
public async Task OnGetAsync(string returnUrl = null)
{
if (!string.IsNullOrEmpty(ErrorMessage))
{
ModelState.AddModelError(string.Empty, ErrorMessage);
}
returnUrl ??= Url.Content("~/");
// Clear the existing external cookie to ensure a clean login process
await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);
ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
ReturnUrl = returnUrl;
}
public async Task<IActionResult> OnPostAsync(string returnUrl = null)
{
returnUrl ??= Url.Content("~/");
ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
if (ModelState.IsValid)
{
// This doesn't count login failures towards account lockout
// To enable password failures to trigger account lockout, set lockoutOnFailure: true
//var result = await _signInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: false);
var result = await _signInManager.PasswordSignInAsync(Input.UserId, Input.Password, Input.RememberMe, lockoutOnFailure: false);
if (result.Succeeded)
{
_logger.LogInformation("ログインしました。");
return LocalRedirect(returnUrl);
}
if (result.RequiresTwoFactor)
{
return RedirectToPage("./LoginWith2fa", new { ReturnUrl = returnUrl, RememberMe = Input.RememberMe });
}
if (result.IsLockedOut)
{
_logger.LogWarning("アカウントロックしました。");
return RedirectToPage("./Lockout");
}
else
{
ModelState.AddModelError(string.Empty, "ログインできませんでした。");
return Page();
}
}
// If we got this far, something failed, redisplay form
return Page();
}
}
}
次にLogin.cshtml
Resend email confirmationのリンクは、メール認証を使わないので消しました。
@page
@model LoginModel
@{
ViewData["Title"] = "ログイン";
}
<h1>@ViewData["Title"]</h1>
<div class="row">
<div class="col-md-4">
<section>
<form id="account" method="post">
<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.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">
<div class="checkbox">
<label asp-for="Input.RememberMe">
<input asp-for="Input.RememberMe" />
@Html.DisplayNameFor(m => m.Input.RememberMe)
</label>
</div>
</div>
<div class="form-group">
<button type="submit" class="btn btn-primary">ログイン</button>
</div>
<div class="form-group">
<p>
<a id="forgot-password" asp-page="./ForgotPassword">パスワードを忘れた場合はこちら</a>
</p>
<p>
<a asp-page="./Register" asp-route-returnUrl="@Model.ReturnUrl">新規アカウント登録</a>
</p>
<!--<p>
<a id="resend-confirmation" asp-page="./ResendEmailConfirmation">Resend email confirmation</a>
</p>-->
</div>
</form>
</section>
</div>
<!--<div class="col-md-6 col-md-offset-2">
<section>
<h4>Use another service to log in.</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>
}
}
</section>
</div>-->
</div>
@section Scripts {
<partial name="_ValidationScriptsPartial" />
}
どうやらUserIdではなくUserNameで探しているみたいです。
あんまりやりたくなかったですが(難しそうだから)、SignInManagerをオーバーライドすることにします。
心が折れた
色々調べたり試したりしたのですが、結局実装できませんでした…。
そもそもASP.NET CoreのIdentityを使っている(カスタマイズしている)記事が全然見つからず、
Coreじゃないものばかり引っかかって、Coreだと違うインターフェースになってるとかで初学者には厳しすぎました。
なので、認証部分はデフォルトのIdentityを使うことにします。
デフォルトのIdentityに戻す
一旦。自分が作ったもの(クラスやらなにやら)は全部消します。
変更したものはもうちょっと後で戻します。(この後、このタイミングで消してよかったと気づく)
1つだけ、DbContextだけはIdentityDbContextを継承した形にします。
以前やったIDのスキャフォールディングをするためです。
using Microsoft.AspNetCore.Identity;
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 <IdentityUser>
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
public DbSet<SalaryManagementSystem.Models.Salary> Salary { get; set; }
public DbSet<IdentityUser> SMSUsers { get; set; }
}
}
この状態でIDをスキャフォールディング。
今回もやっぱり全部チェックを入れておきます。
ビルドエラー解消しなきゃいけないので解消しましょう。
解消するのめんどくさいので、Identityフォルダごと消した方が早いですね。
あとStartup.csにIdentityを設定している個所があるので、それも消します。
Views/Sharedにある_LoginPartical.cshtmlも消します。
スキャフォールディング出来たら、Startup.csは下記の通り修正しておきます。
なぜかここは書かれなかった。
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseNpgsql(
Configuration.GetConnectionString("DefaultConnection")));
services.AddDatabaseDeveloperPageExceptionFilter();
// ここから追加
services.AddDefaultIdentity<IdentityUser>()
.AddDefaultUI()
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>();
// ここまで追加
services.AddControllersWithViews();
services.AddRazorPages();
}
これでいったん起動。あれ、起動するけどDBマイグレーションいらんのか…?
今言わないで。(自分が悪い)
というわけでDBマイグレーションは必要ですね。
ユーザIDとパスワードで認証してユーザ名を表示する方法を考える
今回まででで何が困ったかというと…
・メール認証がいらない(配信サービス使うのは面倒だし個人情報を持ちたくない)
・表示名がメールアドレスになっている(EmailをUserNameにも登録しているから。ここは直せる)
・ユーザIDでログインできない(UserNameでしか検索できない)
おそらくデフォルトだとログイン時のユーザIDと表示名を別々に保持する手立てがなさそう(=独自のユーザクラスを実装する必要がある)なので、今回は、
・メール認証なし
・ユーザID(表示も同じ)とパスワードでログイン
で実装します。
メール認証がいらないので、以前の記事の通り、アカウント登録のメール関連は削除して、
代わりにユーザID(UserNameに登録する)を保持するように作り変えます。
ポイントはInput.UserNameをIdentityUserのUserNameにセットするところ…
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;
namespace SalaryManagementSystem.Areas.Identity.Pages.Account
{
[AllowAnonymous]
public class RegisterModel : PageModel
{
private readonly SignInManager<IdentityUser> _signInManager;
private readonly UserManager<IdentityUser> _userManager;
private readonly ILogger<RegisterModel> _logger;
private readonly IEmailSender _emailSender;
public RegisterModel(
UserManager<IdentityUser> userManager,
SignInManager<IdentityUser> 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 = "ユーザ名")]
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 IdentityUser { UserName = Input.UserName, Email = "" };
user.PasswordHash = new PasswordHasher<IdentityUser>().HashPassword(user, Input.Password);
var result = await _userManager.CreateAsync(user, Input.Password);
if (result.Succeeded)
{
_logger.LogInformation("アカウント登録が完了しました。");
//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();
}
}
}
@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>新しいアカウントを作成します。</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.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">登録</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>
}
}
</section>
</div>-->
</div>
@section Scripts {
<partial name="_ValidationScriptsPartial" />
}
ログインも直してしまいます。
ポイントはUserNameを使うところ。
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;
namespace SalaryManagementSystem.Areas.Identity.Pages.Account
{
[AllowAnonymous]
public class LoginModel : PageModel
{
private readonly UserManager<IdentityUser> _userManager;
private readonly SignInManager<IdentityUser> _signInManager;
private readonly ILogger<LoginModel> _logger;
public LoginModel(SignInManager<IdentityUser> signInManager,
ILogger<LoginModel> logger,
UserManager<IdentityUser> userManager)
{
_userManager = userManager;
_signInManager = signInManager;
_logger = logger;
}
[BindProperty]
public InputModel Input { get; set; }
public IList<AuthenticationScheme> ExternalLogins { get; set; }
public string ReturnUrl { get; set; }
[TempData]
public string ErrorMessage { get; set; }
public class InputModel
{
//[Required]
//[EmailAddress]
//public string Email { get; set; }
[Required]
[Display(Name = "ユーザ名")]
public string UserName { get; set; }
[Required]
[DataType(DataType.Password)]
[Display(Name = "パスワード")]
public string Password { get; set; }
[Display(Name = "記憶する")]
public bool RememberMe { get; set; }
}
public async Task OnGetAsync(string returnUrl = null)
{
if (!string.IsNullOrEmpty(ErrorMessage))
{
ModelState.AddModelError(string.Empty, ErrorMessage);
}
returnUrl ??= Url.Content("~/");
// Clear the existing external cookie to ensure a clean login process
await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);
ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
ReturnUrl = returnUrl;
}
public async Task<IActionResult> OnPostAsync(string returnUrl = null)
{
returnUrl ??= Url.Content("~/");
ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
if (ModelState.IsValid)
{
// This doesn't count login failures towards account lockout
// To enable password failures to trigger account lockout, set lockoutOnFailure: true
var result = await _signInManager.PasswordSignInAsync(Input.UserName, Input.Password, Input.RememberMe, lockoutOnFailure: false);
if (result.Succeeded)
{
_logger.LogInformation("ログインしました。");
return LocalRedirect(returnUrl);
}
if (result.RequiresTwoFactor)
{
return RedirectToPage("./LoginWith2fa", new { ReturnUrl = returnUrl, RememberMe = Input.RememberMe });
}
if (result.IsLockedOut)
{
_logger.LogWarning("ロックされました。");
return RedirectToPage("./Lockout");
}
else
{
ModelState.AddModelError(string.Empty, "ログインに失敗しました。");
return Page();
}
}
// If we got this far, something failed, redisplay form
return Page();
}
}
}
@page
@model LoginModel
@{
ViewData["Title"] = "ログイン";
}
<h1>@ViewData["Title"]</h1>
<div class="row">
<div class="col-md-4">
<section>
<form id="account" method="post">
<!--<h4>Use a local account to log in.</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.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">
<div class="checkbox">
<label asp-for="Input.RememberMe">
<input asp-for="Input.RememberMe" />
@Html.DisplayNameFor(m => m.Input.RememberMe)
</label>
</div>
</div>
<div class="form-group">
<button type="submit" class="btn btn-primary">ログイン</button>
</div>
<div class="form-group">
<p>
<a id="forgot-password" asp-page="./ForgotPassword">パスワードを忘れた</a>
</p>
<p>
<a asp-page="./Register" asp-route-returnUrl="@Model.ReturnUrl">新規アカウント登録</a>
</p>
<!--<p>
<a id="resend-confirmation" asp-page="./ResendEmailConfirmation">Resend email confirmation</a>
</p>-->
</div>
</form>
</section>
</div>
<!--<div class="col-md-6 col-md-offset-2">
<section>
<h4>Use another service to log in.</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>
}
}
</section>
</div>-->
</div>
@section Scripts {
<partial name="_ValidationScriptsPartial" />
}
アカウント登録、ログイン
できました。アカウント登録時に使用する文字列とログイン時に使用する文字列と表示文字列が一致。疲れた。。。
次回予告
・アカウント登録後、ログイン状態にせずログインさせる
・アカウントと給与データを紐づける