.NET Core Identity Manager
在上一篇的文章中
實做了 .NET Core Identity 提供的 IUserStore
介面,用來存取資料庫中的資料,
但是同時也了解到直接使用 Store 意義並不大,這是因為 Identity 當初的設計就是將商業邏輯與底層存取功能分開,
所以我們不該直接使用 IUserStore
而是該使用的是各種的 Manager 方法。
我們來看看 Identity 中常用的 Manager:
- SignInManager
- UserManager
- RoleManager
首先我們先看看 UserManager
的原始碼 Github
會發現 UserManager
之中有注入 IUserStore<TUser>
,在上一篇的文章中建立一個 AccountController
並且註冊了 IUserStore<IdentityUser>
介面,
然後使用 _userStore
存取資料。
但是在實務中,我們應該要在新增會員前做其他額外的檢查,例如確認帳號是否已經存在、密碼是否符合要求等等,需要額外確認後才插入到資料中,
沒有使用 .NET Core Identity 就必須自己實踐這一段的商業邏輯,由於認證與授權是整個系統之中最重要也最基本的功能,
並且沒有經驗的開發人員也可能會寫出有漏洞的程式碼,這時就可以直接使用 .NET Core Identity 中的 UserManager
準備的商業邏輯。
我們先來看看 FindByNameAsync
方法 Github
public virtual async Task<TUser?> FindByNameAsync(string userName)
{
ThrowIfDisposed();
ArgumentNullThrowHelper.ThrowIfNull(userName);
userName = NormalizeName(userName);
var user = await Store.FindByNameAsync(userName, CancellationToken).ConfigureAwait(false);
// Need to potentially check all keys
if (user == null && Options.Stores.ProtectPersonalData)
{
var keyRing = ServiceProvider.GetService<ILookupProtectorKeyRing>();
var protector = ServiceProvider.GetService<ILookupProtector>();
if (keyRing != null && protector != null)
{
foreach (var key in keyRing.GetAllKeyIds())
{
var oldKey = protector.Protect(key, userName);
user = await Store.FindByNameAsync(oldKey, CancellationToken).ConfigureAwait(false);
if (user != null)
{
return user;
}
}
}
}
return user;
}
會發現 UserManager
的 FindByNameAsync
方法會除了將輸入的會員名稱進行搜尋還額外新增了其它檢查邏輯
- 檢查傳入的 userName 是否為空,空值就拋出錯誤
- 將 userName 正規化 (透過注入的 ILookupNormalizer 決定要全部轉成大寫或全部小寫)
- 透過 Store 的
FindByNameAsync
方法取得 User (會使用之前建立的CustomUserStore
的FindByNameAsync
方法) - 確認系統有沒有開啟 ProtectPersonalData 加密,如果有的話要先去 KeyRing 取得密鑰在對 userName 進行加密後查詢 (因為資料庫的 userName 是加密過的直接用明文查詢會查不到)
- 回傳查到的 User 或 Null
另外看一下 CreateAsync
方法
public virtual async Task<IdentityResult> CreateAsync(TUser user)
{
ThrowIfDisposed();
await UpdateSecurityStampInternal(user).ConfigureAwait(false);
var result = await ValidateUserAsync(user).ConfigureAwait(false);
if (!result.Succeeded)
{
return result;
}
if (Options.Lockout.AllowedForNewUsers && SupportsUserLockout)
{
await GetUserLockoutStore().SetLockoutEnabledAsync(user, true, CancellationToken).ConfigureAwait(false);
}
await UpdateNormalizedUserNameAsync(user).ConfigureAwait(false);
await UpdateNormalizedEmailAsync(user).ConfigureAwait(false);
return await Store.CreateAsync(user, CancellationToken).ConfigureAwait(false);
}
- 如果 Store 有實現 IUserSecurityStampStore 界面就利用 store 的 SetSecurityStampAsync 方法更新 User 的 SecurityStamp
- 呼叫 ValidateUserAsync 方法,內部會根據註冊的 UserValidators 進行基本的帳號不重複檢查或 Email不重複檢查
- 檢查系統設定是否有開啟 UserLockout(登入錯誤次數過多自動鎖定)
- 利用 Store 的 SetNormalizedUserNameAsync 方法更新 User 的 NormalizedUserName
- 如果 Store 有實現 IUserEmailStore 界面就利用 store 的 SetNormalizedEmailAsync 方法更新 User 的 NormalizedEmail
看玩 CreateAsync
跟 FindByNameAsync
它們的內部商業邏輯後發現還有幾個 Store 方法目前還沒有實做所以需要先回到 CustomUserStore
實做這幾個方法
- GetUserIdAsync
- GetUserNameAsync
- SetNormalizedUserNameAsync
public Task<string> GetUserIdAsync(IdentityUser user, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
if (user == null)
{
throw new ArgumentNullException(nameof(user), $"Parameter {nameof(user)} cannot be null.");
}
return Task.FromResult(user.Id);
}
public Task<string?> GetUserNameAsync(IdentityUser user, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
if (user == null)
{
throw new ArgumentNullException(nameof(user), $"Parameter {nameof(user)} cannot be null.");
}
return Task.FromResult(user.UserName);
}
public Task SetNormalizedUserNameAsync(
IdentityUser user,
string? normalizedName,
CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
if (user == null)
{
throw new ArgumentNullException(nameof(user), $"Parameter {nameof(user)} cannot be null.");
}
user.NormalizedUserName = normalizedName;
return Task.CompletedTask;
}
完成之後準備修改 AccountController
,從原本的 IUserStore
改成註冊 UserManager
,因為 UserManager 內部會自行處理帳號大小寫問題
所以自己就不用額外處理了。
[ApiController]
[Route("[controller]")]
public class AccountController : ControllerBase
{
private readonly UserManager<IdentityUser> _userManager;
public AccountController(UserManager<IdentityUser> userManager)
{
_userManager = userManager;
}
[HttpPost("CreateUser")]
public async Task<IdentityResult> CreateUser(string userName)
{
var identityUser = new IdentityUser(userName);
return await _userManager.CreateAsync(identityUser);
}
[HttpGet("GetUser")]
public async Task<IdentityUser?> GetUser(string userName)
{
return await _userManager.FindByNameAsync(userName);
}
}
接下來就可以進行測試,先 Post https://localhost:7057/Account/CreateUser?userName=user2
建立一個新的會員 user2
,
由於沒有實現 IUserSecurityStampStore 與 IUserEmailStore 所以會略過這部份的設定。
建立完成後 Get https://localhost:7057/Account/GetUser?userName=user2
取得資料庫的會員,發現 NormalizedUserName 有確實更新到資料庫內
如果再次 Post https://localhost:7057/Account/CreateUser?userName=user2
會回傳錯誤結果提示 Username 重複了
{
"succeeded": false,
"errors": [
{
"code": "DuplicateUserName",
"description": "Username 'user2' is already taken."
}
]
}
Summary
今天學習了 UserManager 中 CreateAsync
跟 FindByNameAsync
內部的邏輯並且成功改寫我們的 AccountController 將原本的 UserStore 改成註冊 UserManager
這樣就多了許多保護邏輯讓我們的 Api 不用寫額外的程式就能有可靠的商業邏輯可以使用,但如果還是覺得預設的 UserManager 方法不夠用
我們可以建立另一個 Class 繼承 UserManager, 因為原本的方法都是 virtual 所以可以輕易的用 override 對方法進行改寫加入自己的商業邏輯,客製化程度相當高
關於 .NET Core Identity 還有許多的功能之後的文章會再進行討論。