--- title: 帳號系統開發筆記 tags: [.Net 8,帳號驗證,MVC,blog] --- {%hackmd @hackmd/blog-article-base-layout %} <style> .blog-date { margin-top: 60px; } .blog-date.no-margin { margin-top: 20px; } body .ui-toc, #ui-toc-affix { display: none !important; } body > #doc.markdown-body { padding-top: 20px; } #doc.markdown-body h2 { margin-top: 5px; } </style> :arrow_left: [回到部落格首頁](/@siugar/blog-tw) :arrow_right: <a target="_blank" href="/@siugar/AccountSystem">本文單篇連結</a> # 帳號系統開發筆記 <div class="blog-date no-margin">2025/2/22</div> <br/> ## 前言 因為新的專案全新開發,採用.Net 8 MVC框架,需要後台帳號系統,所以從無到有完成帳號管理與驗證系統 ## 功能主要說明 建立帳號,套用不同角色,可以有不同的權限與帳號驗證登入 ## 功能細節 1. 使用者管理 1.1 新增,修改,刪除 1.2 修改使用者密碼 2. 角色管理 2.1 新增,修改,刪除 2.2 權限 - 2.2.1 超級管理員 - 2.2.2 帳號管理 - 2.2.3 所有商店管理(編輯或觀看) - 2.2.4 圖片管理(編輯或觀看) - 2.2.5 跑馬燈管理(編輯或觀看) - 2.2.6 報表管理 3. 登入 3.1 密碼驗證 3.2 失敗三次鎖定30分鐘 4. 個人資訊 4.1 顯示個人資訊 4.2 修改個人密碼 ## 模組拆分 1. Account: 1.1 UserMgr 使用者管理模組 1.2 RoleMgr 角色管理模組 2. Auth 2.1 Auth 驗證模組 - 2.1.1 Login 登入 - 2.1.2 InitAcount 站台帳號初始化 2.2 PermissionHandler 權限檢查入口 2.3 PermissionService 權限處理Service Controller,UseCase, Repository是拆分在不同的project,比方說UseCase隸屬於Domain project,但為了方便說明,就沒有再將Clean Architecture的分專案與檔案結構對應呈現出來。 ![image](https://hackmd.io/_uploads/H1Vam3rqJl.png) ## 資料規劃 ### 靜態資料 ```json "Permission": { "Policy": [ { "Name": "SuperAdmin", "Id": 10, "DisplayName": "超級管理員" }, { "Name": "AccountManage", "Id": 20, "DisplayName": "帳戶管理" }, { "Name": "EditImageManage", "Id": 30, "GroupName": "圖片管理", "DisplayName": "編輯圖片管理" }, { "Name": "ViewImageManage", "Id": 35, "GroupName": "圖片管理", "DisplayName": "觀看圖片管理" } ] } ``` 我是將Permission設定放在靜態資料內,比如appsetting.json,原因是因為這一塊如果需要修改多是因為要新增功能或是UI顯示面修改,當作靜態設定會比放在資料庫更方便修改與對應程式版本 ### 動態資料 - Db 表格規劃 ![image](https://hackmd.io/_uploads/ryl65IHckl.png) #### 1. 主要拆分成User與Role的表格,和使用者相關的資料擺放在User table, 和角色相關的放在Role table #### 2. User與Role的關聯性用RolesIds來關連而非新建UserRole表格 2.1 增加關聯性表格實作管理複雜且不利肉眼查詢與除錯 因為多建立這個表格雖然有利於sql查詢,但真正在實作更新角色關聯性相關功能時相對比較麻煩,且增加這張表格不利於肉眼查詢或除錯,單純看一張表格就可以看出資料的關聯性,一開始我有設計但程式碼寫到一半後都改掉了。 2.2 採用JSON格式 格式就是在c#端從IEnumerable<int>轉成Json存放到資料庫,採用db Json 類型的好處是後續sql查詢時可以運用資料庫的內建Json相關函式,這樣可以增加sql查詢的彈性。 #### 賦予多個角色會有權限重疊的狀況 下圖這種權限重疊的狀況是會發生的,所以一定要考量到 ![image](https://hackmd.io/_uploads/HkTIYjrqkl.png) ## 部分畫面展示 ### 1. 使用者管理 ![使用者管理_Mask](https://hackmd.io/_uploads/BJ3H3eo9kl.png) ### 2. 建立使用者 ![建立使用者_Mask](https://hackmd.io/_uploads/ryeZ6licJg.png) ### 3. 角色管理 ![角色管理_Mask](https://hackmd.io/_uploads/H1jZ6ejq1e.png) ### 4. 新增角色 ![新增角色Full_Mask](https://hackmd.io/_uploads/H1aMpgj5yl.png) ## 技術細節介紹 ### 1. Policy-based 授權[^PolocyBased] #### 主要需求點: 1.1 要可以動態建立角色(Role)與修改角色(Role)包含的權限 1.2 後台要可以開啟多台 1.3 動態變更權限後,可以盡快生效而非要使用者重新登入 使用這種方法可以在每次觸發Action或Api時檢查權限,透過自訂的 PermissionHandler 來驗證權限,最優化的做法是將權限資料放在redis,而我因為預期實際同時使用者人數不會太多,每次可以先到資料庫查詢,未來真的有性能壓力出現時,需要優化再搬到redis都還是有機會的。 #### 前端範例 - 示範判斷EditImageManage是否存在來決定是否顯示"圖片新增"按紐 ```html @inject IAuthorizationService AuthorizationService @{ var permissionEdit = await AuthorizationService.AuthorizeAsync(User, "EditImageManage"); var changeable = permissionEdit.Succeeded; @if (changeable) { <form asp-action="GoAddYourImageView" asp-controller="YourImageMgr" method="get"> <button type="submit" class="btn btn-primary">圖片新增</button> </form> } } ``` #### 後端範例 - 透過Policy控制整個Controller與特定的Action對應不同的Policy 以下面範例來說,擁有ViewImage權限的人可以訪問整個ImageMgrController但只有EditImage權限的人 ```csharp [Authorize(Policy = "ViewImage")] public class ImageMgrController : Controller { //節略 public async Task<IActionResult> Index() { IEnumerable<Image>? resultList = await _queryImageRepository.GetImageList(500, 0); return View(_mapper.ToViewModel(resultList)); } [Authorize(Policy = "EditImage")] public async Task<IActionResult> AddImage(AddImageReq req, IFormFile? file, [FromServices] AddImageUseCase useCase) { //節略 } } ``` ### 2. 資料庫User Table採用Argon2演算法先加鹽再雜湊加密 將使用者密碼運算Hash後存在資料庫是常見的做法,然後登入時,帳密驗證再次密碼Hash比對,這做法如果資料庫的User表格洩漏出去很容易所有的密碼驗證就會被破解,所以通常會再外加一段salt,強化被破解的難易度,而怎麼算出hash以及salt就有很多演算法。 目前我自己實作或是整合套建了三種演算法:SHA256,BCrypt與Argon2。一開始我自己寫了SHA256的做法,但後來發現這方法比較沒那麼安全所以找了插件BCrypt.Net-Next實裝了BCrypt,最後實際採用BouncyCastle.Cryptography的Argon2,他是2015年密碼雜湊競賽(PHC)冠軍,抗破解能力最強,所以我選擇了這一種。 參考資料[^Argon2參數推薦]我有提供推薦的Argon2的參數設定,有興趣的可以參考看看。 ## 結論 雖然開發完成後現在看起來似乎理所當然分成UserMgr, RoleMgr與Auth。但當時因為第一次開發且因為專案需要拆分階段開發,先從登入流程開始寫,後面才慢慢補上UserMgr與RoleMgr,當初一開始寫時是建立了一個AccountMgr負責Login與初始化帳號創建,中間持續開發時發現會遇到User,Role表格將會共用於AccounMgr以及UserMgr,導致破壞單一功能原則(SRP),所以最後在功能開發告一段落後做了重構,將AccountRepository抽到UserRepository與RoleRepository,避免多個地方都去接觸資料庫,最終改成現在這個結構上比較清晰的版本。 ## 參考資料 [^PolocyBased]: [Policy-based authorization in ASP.NET Core](https://learn.microsoft.com/en-us/aspnet/core/security/authorization/policies?view=aspnetcore-8.0) [^Argon2參數推薦]: [Owasp的Argon2參數推薦](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#argon2id)