iT邦幫忙

2023 iThome 鐵人賽

DAY 27
0
SideProject30

我想自己刻部落格系列 第 27

建立註冊、登入、登出

  • 分享至 

  • xImage
  •  

終於要開始寫註冊、登入、登出功能了。

為了確認成功與否,我先定義了 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;
}

CreateUser 動作與檢視

而在 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>

建立完的結果
https://ithelp.ithome.com.tw/upload/images/20231012/20120420KZ92eqDNHb.png

實作登入功能

建立模型

這邊我用信箱與密碼來驗證,標上驗證屬性

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()

在 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);
}

LoginController

在控制器這邊要做的事就很少了,實作 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 。可以使用自動產生或是自己建。

產生的樣式會如下。

https://ithelp.ithome.com.tw/upload/images/20231012/20120420CnLc3qDh6s.png

實際程式碼以 GitHub 上為主。


上一篇
建立帳號相關 UserService
下一篇
個人資料變更 UpdateUser
系列文
我想自己刻部落格31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言