使用者驗證與授權 - ASP.NET Core Identity :使用者註冊與登入

昨天在稍微了解了 ASP.NET Core Identity 會員管理機制之後,已經建立了用來記錄使用者登入以及授權旳資料庫,並將原有的控制器加上了 [Authorize] 授權要求,這樣一來,未經授權的使用者就無法使用我們所開發的 Web API 了。

但是,問題來了,那誰又是合法的授權者呢?要在什麼條件下,才可以使用呢?所以今天的學習目標就訂在《後端》的使用者註冊與登入吧!

使用者登入模型

一般來講常見的使用者認證(Authentication)方式,會讓使用者輸入(或提供)一組帳號和密碼,經過比對儲存在會員管理的資料庫中是否存在該組帳號和密碼,如果存在就判定為合法使用者,並標示為登入成功,反之資料庫中不存在該組帳號和密碼就判定為登入失敗。

因此,先新增一個用來傳遞使用者帳號和密碼的模型:

using System.ComponentModel.DataAnnotations;

namespace Demae.Core.Models
{
    public class LoginModel
    {
        [Required]
        [EmailAddress]
        [Display(Name = "電子郵件帳號")]
        public string Email { get; set; }

        [Required]
        [StringLength(30, ErrorMessage = "{0} 長度必須最少 {2} 以及最多 {1} 個字元。", MinimumLength = 6)]
        [DataType(DataType.Password)]
        [Display(Name = "密碼")]
        public string Password { get; set; }
    }
}
註:
一般在會員管理的設計上,有時會同時將電子郵件帳號就當成使用者帳號來使用,也有的設計是使用者帳號歸與電子郵件帳號同時使用。這樣的設計方式各有優缺點,因為在一個系統中,使用者帳號通常會要求是唯一的值,而開放讓外部的人申請帳號時,要求以電子郵件帳號當作使用者帳號比較容易達成唯一值的需求(否則可能一大堆人都要叫 tom 或 mary 等好記的英文名稱),但是使用電子郵件帳號又有可能造成使用者因離職或其他原因造成該電子郵件帳號無法再使用。

使用者註冊模型

因為使者註冊所需要的資訊除了帳號和密碼之外,還要加入一些額外的資訊(本案例是增加暱稱),所以就直接繼承先前的登入模型再追加需要的屬性就好了:

using System.ComponentModel.DataAnnotations;

namespace Demae.Core.Models
{
    public class RegisterModel : LoginModel
    {
        [Required]
        [Display(Name = "暱稱")]
        public string Nickname { get; set; }
    }
}
註:
一般來講,在設計一個系統之前首先會考慮該系統的目標使用者,才再決定如何註冊和管理會員(會員只是一個統稱,如果是企業員工使用就不該叫會員了)。比如說,這個系統如果是企業內部員工使用,當然不可能隨便讓人自由註冊,應該是人事單位在新人報到時,輸入員工基本資料後,自動註冊才對。目前就先當作練習,先讓只要能連線並知道 URL 的人就能註冊,往後有機會再來討論各種情境的註冊方式。

帳號管理控制器

接著新增一個空白的 API 控制器:

ASP.NET Core Identity 已經實作了使用者管理和登入的 UserManager 和 SignInManager 所以在控制器的建構函式中直接拿來用就好了:

namespace Demae.Api.Controllers
{
    [Produces("application/json")]
    [Route("api/Account")]
    public class AccountController : Controller
    {
        private UserManager<AppUser> _userManager;
        private SignInManager<AppUser> _signInManager;
        public AccountController(
            UserManager<AppUser> userManager,
            SignInManager<AppUser> signInManager)
        {
            _userManager = userManager;
            _signInManager = signInManager;
        }
        
        // 省略


    }
}

接著加入註冊使用者的 POST 方法:

namespace Demae.Api.Controllers
{
    [Produces("application/json")]
    [Route("api/Account")]
    public class AccountController : Controller
    {
        // 省略

        [HttpPost]
        public async Task<IActionResult> Create([FromBody] RegisterModel model)
        {
            if (!ModelState.IsValid)
            {
                return
                    BadRequest(
                        ModelState.Values.SelectMany(v => v.Errors)
                            .Select(modelError => modelError.ErrorMessage)
                            .ToList());
            }

            var user = new AppUser
            {
                UserName = model.Email,
                Email = model.Email,
                NickName = model.Nickname
            };
            var result = await _userManager.CreateAsync(user, model.Password);

            if (!result.Succeeded)
            {
                return BadRequest(result.Errors.Select(x => x.Description).ToList());
            }

            await _signInManager.SignInAsync(user, false);

            return Ok();
        }

        // 省略
    }
}

接著加入登入的 POST 方法:

namespace Demae.Api.Controllers
{
    [Produces("application/json")]
    [Route("api/Account")]
    public class AccountController : Controller
    {
        // 省略

        [HttpPost("login")]
        public async Task<IActionResult> Login([FromBody] LoginModel model)
        {
            if (!ModelState.IsValid)
            {
                return BadRequest();
            }

            var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, isPersistent: false,
                lockoutOnFailure: false);

            if (!result.Succeeded)
            {
                return BadRequest();
            }

            return Ok();
        }
    }
}

成果測試

雖然還沒有實作 Xamarin.Forms 的使用者註冊和登入介面,但是還是可以使用 Postman 測試功能是否正常,看來密碼必需至少包含一個特殊字元和一個大寫字母:

看來也必需至少包含一個小寫字母,所以改了密碼之後註冊成功,證明功能正常:

因為先前實作的註冊方法,在註冊成功後也會一起登入,所以這時不用再登入即可正常執行查詢功能:

註:
也有系統會規畫成註冊完後必需等驗證完電子郵件帳號或是行動電話號碼是否為真實存在,才會再讓使用者登入,這個課題留待往後再討論。

登入功能也順道測試一下,也是確定功能正常:

好吧,今天就暫且先學習到這裡,明天再來學習實作 Token Based Authentication 吧!