終於要開始寫註冊、登入、登出功能了。
為了確認成功與否,我先定義了 ResultViewModel 用來回傳結果。
public class ResultViewModel
{
    public bool IsSuccess { get; set; }
    public List<string> Messages { get; set; } = new List<string>();
    public ResultViewModel(bool isSuccess, string messages)
    {
        IsSuccess = isSuccess;
        Messages.Add(messages);
    }
    public ResultViewModel(bool isSuccess, List<string> messages)
    {
        IsSuccess = isSuccess;
        Messages = messages;
    }
    public void AddError(string errorMessage)
    {
        Messages.Add(errorMessage);
        IsSuccess = false;
    }
}
另外我也調整了昨天 UserService 下面的 InitCreateAsync()。畢竟個人部落格,其實不用很多帳號,簡單處理即可。
public async Task<ResultViewModel> InitCreateAsync(UserViewModel viewModel)
{
    int count = GetCount();
    if (count == 0)
    {
        viewModel.Role = (int)RoleStatus.管理員;
        await CreateAsync(viewModel);
        return new ResultViewModel(true, "註冊成功");
    }
    return new ResultViewModel(false, "不開放註冊其他帳號");
}
public int GetCount()
{
    var count = _userRepository.GetAll().Count();
    return count;
}
而在 InitController 這邊,定義 CreateUser() 動作,作為我們第一次註冊來使用,如果資料庫內已經有帳號,就不能使用
public IActionResult CreateUser()
{
    return View();
}
[HttpPost]
public async Task<IActionResult> CreateUser(UserViewModel viewModel)
{
    if (ModelState.IsValid)
    {
        var result = await _userService.InitCreateAsync(viewModel);
        if (result.IsSuccess)
        {
            return RedirectToAction("Login", "Login");
        }
        ModelState.AddModelError(string.Empty, result.Messages.First());
    }
    return View(viewModel);
}
建立 View 一樣使用樣板產生器,使用 Create 模板幫我們產生,或是自己建立。使用的資料模型是UserViewModel
@model MyBlog.Models.UserViewModel
<div class="row">
    <div class="col-md-4">
        <form asp-action="CreateUser">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="Email" class="control-label"></label>
                <input asp-for="Email" class="form-control" />
                <span asp-validation-for="Email" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Password" class="control-label"></label>
                <input asp-for="Password" class="form-control" />
                <span asp-validation-for="Password" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="PasswordCheck" class="control-label"></label>
                <input asp-for="PasswordCheck" class="form-control" />
                <span asp-validation-for="PasswordCheck" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Name" class="control-label"></label>
                <input asp-for="Name" class="form-control" />
                <span asp-validation-for="Name" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="DisplayId" class="control-label"></label>
                <input asp-for="DisplayId" class="form-control" />
                <span asp-validation-for="DisplayId" class="text-danger"></span>
            </div>
            <div class="form-group">
                <input type="submit" value="Create" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>
建立完的結果
這邊我用信箱與密碼來驗證,標上驗證屬性
public class LoginViewModel
{
    [Required]
    [EmailAddress]
    public string Email { get; set; } = string.Empty;
    [Required]
    [DataType(DataType.Password)]
    public string Password { get; set; } = string.Empty;
}
在 UserService 加入 LoginAsync 方法來做登入判斷,因為我們的密碼有做雜湊,所以判斷登入的方式比較不一樣,要先找出這個帳號使用者的User物件,與密碼一起才能做比較。
public async Task<ResultViewModel> LoginAsync(LoginViewModel viewModel)
{
    var user = _userRepository.Query(x => x.Email == viewModel.Email).FirstOrDefault();
    if (user != null
        && PasswordVerificationResult.Success == VerifyHashedPassword(user, viewModel.Password))
    {
        await CreateCookieAsync(user);
        return new ResultViewModel(true, "登入成功");
    }
    return new ResultViewModel(false, "帳號或密碼錯誤");
}
private PasswordVerificationResult VerifyHashedPassword(User user, string password)
{
    return _passwordHasher.VerifyHashedPassword(user, user.Password, password);
}
確定帳號密碼正確之後,就要建立並使用 Cookie 登入 。寫入用戶資料,設定長期記憶Cookie IsPersistent = true 。
最後使用 HttpContext.SignInAsync() 來做登入。而在 Server 層,不像 Controller 層一樣有 HttpContext 可以使用,我們需要注入 IHttpContextAccessor 來提供我們 HttpContext,才可以。
Program.cs 需要註冊以下內容
builder.Services.AddTransient<IUserService, UserService>();
builder.Services.AddScoped<IPasswordHasher<User>, PasswordHasher<User>>();
builder.Services.AddHttpContextAccessor();
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie();
另外 Program.cs 下方也要啟動身分驗證與授權,上下順序固定不可調整。
app.UseAuthentication();
app.UseAuthorization();
下面這段就是建 Cookie 的程式碼。
private async Task CreateCookieAsync(User user)
{
    var claims = new List<Claim>
    {
        new Claim(nameof(User.Id),user.Id.ToString()),
        new Claim(ClaimTypes.Name, user.Name),
        new Claim(ClaimTypes.Email, user.Email),
        new Claim(ClaimTypes.Role,user.Role.ToString()),
    };
    var authProperties = new AuthenticationProperties
    {
        IsPersistent = true,
    };
    var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
    await _accessor.HttpContext.SignInAsync(new ClaimsPrincipal(claimsIdentity), authProperties);
}
在控制器這邊要做的事就很少了,實作 Login() 與 Logout() 就可以了。
public IActionResult Login()
{
    return View();
}
[HttpPost]
public async Task<IActionResult> Login(LoginViewModel viewModel)
{
    if (ModelState.IsValid)
    {
        var result = await _userService.LoginAsync(viewModel);
        if (result.IsSuccess)
        {
            return RedirectToAction("Index", "Home");
        }
        ModelState.AddModelError("Email", result.Messages.First());
    }
    return View(viewModel);
}
public async Task<IActionResult> Logout()
{
    await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
    return RedirectToAction("Index", "Home");
}
登出跟登入很像,只要換用HttpContext.SignOutAsync()就可以登出了。
Login View 的模型就是前面定義的 LoginViewModel 。可以使用自動產生或是自己建。
產生的樣式會如下。

實際程式碼以 GitHub 上為主。