[ASP.NET Core MVC] 使用一般 Controller 建立 Web Api 與實作 Client 端呼叫
此範例我會教學我如何建立 Web Api 專案,然後從客戶端呼叫 Web Api 的方法,其中包含基本的驗證方法。
我一共會建立 3 個專案,分別是 ASP.NET Core MVC 的 Web Api 與 Winform Form 當作客戶端,另外新增一個類別庫專案,做為兩者間共用類別庫。
我會以常見的會員登入做為示範,在 Web Api 接收帳密,並驗證資料庫帳密,在 Windows Form 傳送帳密資料。
主流教 ASP.NET 的 Web Api 會建立 Api Controller,可是我不喜歡這麼做,我喜歡用一般的控制器 (Controller) 來建立 WebApi 就好,會這樣做有幾點原因:
- Api Controller 是遵循 RESTful API 的設計風格而寫 Web Api,但是我個人覺得 RESTful API 不是那麼好用,我不會用到 PUT, DELETE 這種方式來定義動作,我都統一用 Post 而已,這樣寫法比較單純,動作就用方法名稱區別再搭配文件說明。
- RESTful API 用 Http 狀態 (例如 200,400, 500) 來定義成功、失敗或其他訊息,我通常會自行處理錯誤訊息,統一用成功狀態 200 回傳,有錯誤訊息會寫在自定 Model 裡面。
- 因為 Api Controller 預設的底層類別為 ControllerBase 與一般的 Controller 不一樣。我習慣建立一般 Controller 的底層類別,自行覆寫生命週期動作,例如 OnActionExecuting, OnActionExecuted 等方法,這個底層類別可以讓一般 Controller 繼承,同樣也適合用在 Web Api 類別繼承。
可能我的方法是非主流,教學內容也加入許多個人經驗,有興趣的人可以多學習一種做法。
Contents
建立 ASP.NET Core MVC 專案
打開 VS 2022,新增專案選擇「ASP.NET Core Web 應用程式 (Model-View-Controller)。
輸入專案名稱為 “WebApiServer”,選擇位置,解決方案名稱我用 ” WebApiSample”
架構就預設的 .NET 6
專案建立後,在 Controllers 資料夾加入「控制器」,這裡注意要用 MVC 的「MVC 控制器-空白」,而不是選「API」的控制器喔。
類別名稱為 “ApiController.cs”,此類別是繼承 Controller。
登入驗證方法
在 ApiController 加入一個方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
/// <summary> /// 登入檢查 /// </summary> /// <param name="req"></param> /// <returns></returns> public IActionResult Login([FromBody] LoginReq req) { LoginResp resp = new LoginResp(); //檢查欄位必填 if (ModelState.IsValid == false) { string ErrMsg = string.Join("\n", ModelState.Values.SelectMany(x => x.Errors).Select(x => x.ErrorMessage)); resp.ErrMsg = ErrMsg; return Json(resp); } // 檢查驗證碼 try { // Api 密鑰 IConfiguration Config = new ConfigurationBuilder().AddJsonFile("appSettings.json").Build(); string encyptKey = Config.GetSection("EncyptKey").Value; string tokenData = DecryptAES256(req.Token, encyptKey, null); // 檢查時間有效內 TimeSpan TS = new System.TimeSpan(Convert.ToDateTime(tokenData).Ticks - DateTime.Now.Ticks); double timeDiff = Convert.ToDouble(TS.TotalMinutes); //分鐘差異 if (timeDiff > 5 || timeDiff < -5) { resp.ErrMsg = "驗證碼已過期"; return Json(resp); } } catch { resp.ErrMsg = "驗證碼格式錯誤"; return Json(resp); } // 檢查資料庫帳密正確 string loginId = req.LoginId; string loginPwd = req.LoginPwd; bool loginCheck = true; //資料庫檢查結果 //省略資料庫查詢動作... //直接回傳帳密正確的結果 if (loginCheck) { resp.UserName = "王小明"; } else { resp.ErrMsg = "帳號密碼錯誤"; } return Json(resp); } |
這裡我省略資料庫查詢的語法,通常就是將帳號密碼在資料庫內執行 Select 語法,來檢查是否有符合。
通常我用於判斷 Api 回傳正確或失敗的方式,是檢查 ErrMsg
這個變數有沒有值,如果有值就表示錯誤,而值就是錯誤訊息,null 或空值都是正確,這只是我的習慣方式。
你也可以用個人習慣的方式來判斷正確或失敗。
檢查欄位必填與檢查驗證碼,是每個 Api 都很重要的部份,檢查驗證碼可以阻擋惡意攻擊,當這些檢查方法在每個 Api 方法都需要用到時,就可以抽離至外部方法自動執行,建議可使用剖面導向程式設計 (AOP) 來執行,可參考我這篇文章的做法。
這個方法用到的新類別 LoginReq, LoginResp
,都會建立在共用類別庫,因為這兩個 Model 在 Client 端也會用到,所以我統一放在共用裡面。
建立共用類別庫
因為這專案會有 Web 專案與 Windown Form 專案,兩者專案無法互相引用,而兩者又有相同的類別定義,例如 Api 方法的 Model,所以我會將相同的 Api Model 放在共用類別庫。
在現有的方案,按右鍵,加入「新增專案」。
在「程式庫」的分類下可以找到「類別庫」
我取名為 “CommonLibrary”
架構用預設「.NET 6」
共用 Model
在共用類別庫,我新增一資料夾,取名為 ”ApiModel”,這次的範例功能是登入,所以我在 ”ApiModel” 資料夾下,新增類別取名叫 “LoginApiModel.cs”
在 LoginApiModel 類別,最主要兩個 Model 是定義登入的呼叫與回傳,所以我新增兩個 Model 定義:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
public class LoginApiModel { public class LoginReq : ApiModelBase { /// <summary> /// 登入 ID /// </summary> public string LoginId { get; set; } /// <summary> /// 登入密碼 /// </summary> public string LoginPwd { get; set; } } public class LoginResp : ApiModelBase { /// <summary> /// 使用者名稱 /// </summary> public string UserName { get; set; } } } |
這裡有一個新類別是 ApiModelBase
,是所有 Api Model 都要繼承的底層類別,所以我在 ApiModel 資料夾下再新增新類別 “ApiModelBase.cs”。
ApiModelBase 語法為:
1 2 3 4 5 6 7 8 9 10 11 12 |
public class ApiModelBase { /// <summary> /// 驗證碼 /// </summary> public string Token { get; set; } /// <summary> /// 錯誤訊息 /// </summary> public string ErrMsg { get; set; } = ""; } |
在 WebApi 專案加一個共用類別庫很好用,可以將兩者都用到的方法及 Model 放在這裡。
新增 Windows Form 客戶端
接下來示範客戶端的呼叫,在方案加入一個專案。
選擇「Windows Forms 應用程式」。
專案名稱我取名為 “WinFormsClient”
架構用預設的「.NET 6」,建立專案。
接著在預設的 Form1 ,修改介面,簡單做一個登入畫面。
客戶端呼叫 WebApi
雙擊「登入」鈕,寫入此語法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
private void btnLogin_Click(object sender, EventArgs e) { try { LoginReq req = new LoginReq(); req.LoginId = txtLoginId.Text; //帳號 req.LoginPwd = txtLoginPwd.Text; //密碼 // Api 密鑰 (建議存在設定檔內) string EncyptKey = "123456"; // Api 驗證碼 string nowTime = DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss"); string token = EncryptAES256(nowTime, EncyptKey, null); req.Token = token; // Api Url string url = "https://localhost:7151/Api/Login"; // 呼叫 Web Api 登入檢查 LoginResp resp = CallApi<LoginResp>(url, req); // ErrMsg 有值表示登入失敗 if (!string.IsNullOrEmpty(resp.ErrMsg)) { // 登入錯誤 MessageBox.Show(resp.ErrMsg); } else { // 登入成功 MessageBox.Show("登入成功"); } } catch (Exception ex) { MessageBox.Show(ex.Message); } } |
呼叫 WebApi 建議帶上一組驗證碼,由彼此指定好的私鑰來產生驗證碼,其他沒有驗證碼、無效或過期的驗證碼,都要阻擋下來,避免惡意來源攻擊 WebApi。
驗證碼通常我會帶上時間,讓這組驗證碼只在短時間內有效,過期就要重新產生。
我將呼叫 WebApi 寫成方法,使用泛型來指定回傳型別。
CallApi 方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
/// <summary> /// 呼叫 Api /// </summary> /// <param name="param"></param> /// <returns></returns> public static T CallApi<T>(string apiUrl, ApiModelBase req) { string json = JsonConvert.SerializeObject(req); var data = new StringContent(json, Encoding.UTF8, "application/json"); var responseTask = httpClient.PostAsync(apiUrl, data); responseTask.Wait(); var result = responseTask.Result; var readTask = result.Content.ReadAsStringAsync(); readTask.Wait(); if (result.IsSuccessStatusCode) { json = readTask.Result; return JsonConvert.DeserializeObject<T>(json); } else { throw new Exception(result.ReasonPhrase + "\n" + readTask.Result); } } |
最後附上 Server 與 Client 都有用到的加解密方法,我是用 AES256 CBC 加解密的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 |
/// <summary> /// 使用 AES 256 加密 /// </summary> /// <param name="source">本文</param> /// <param name="key">密鑰</param> /// <param name="iv">IV</param> /// <returns></returns> public static string EncryptAES256(string source, string key, string? iv) { // IV 為 16 個英文或數字 if (string.IsNullOrEmpty(iv)) { iv = "1234567890abcdef"; } // 產生 MD5 32 字串密鑰 string md5Key = ""; using (MD5 md5Serv = MD5.Create()) { byte[] keyArray = md5Serv.ComputeHash(Encoding.UTF8.GetBytes(key)); md5Key = BitConverter.ToString(keyArray).Replace("-", "").ToUpper(); } // 使用 AES 加密 byte[] sourceBytes = Encoding.UTF8.GetBytes(source); string encryptStr = ""; using (Aes aes = Aes.Create()) { aes.Key = Encoding.UTF8.GetBytes(md5Key); aes.IV = Encoding.UTF8.GetBytes(iv); aes.Mode = CipherMode.CBC; aes.Padding = PaddingMode.PKCS7; ICryptoTransform transform = aes.CreateEncryptor(); encryptStr = Convert.ToBase64String(transform.TransformFinalBlock(sourceBytes, 0, sourceBytes.Length)); } return encryptStr; } /// <summary> /// 使用 AES 256 解密 /// </summary> /// <param name="encryptData">密文</param> /// <param name="key">密鑰</param> /// <param name="iv">IV</param> /// <returns></returns> public static string DecryptAES256(string encryptData, string key, string? iv) { // IV 為 16 個英文或數字 if (string.IsNullOrEmpty(iv)) { iv = "1234567890abcdef"; } // 產生 MD5 32 字串密鑰 string md5Key = ""; using (MD5 md5Serv = MD5.Create()) { byte[] keyArray = md5Serv.ComputeHash(Encoding.UTF8.GetBytes(key)); md5Key = BitConverter.ToString(keyArray).Replace("-", "").ToUpper(); } // 使用 AES 解密 var encryptBytes = Convert.FromBase64String(encryptData); string decryptStr = ""; using (Aes aes = Aes.Create()) { aes.Key = Encoding.UTF8.GetBytes(md5Key); aes.IV = Encoding.UTF8.GetBytes(iv); aes.Mode = CipherMode.CBC; aes.Padding = PaddingMode.PKCS7; ICryptoTransform transform = aes.CreateDecryptor(); decryptStr = Encoding.UTF8.GetString(transform.TransformFinalBlock(encryptBytes, 0, encryptBytes.Length)); } return decryptStr; } |
範例下載
相關學習文章
如果你在學習上有不懂的地方,需要諮詢服務,可以參考站長服務,我想辨法解決你的問題
如果文章內容有過時、不適用或錯誤的地方,幫我在下方留言通知我一下,謝謝