此範例我會教學我如何建立 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 類別繼承。
可能我的方法是非主流,教學內容也加入許多個人經驗,有興趣的人可以多學習一種做法。
建立 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 語法為:
|
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; } |
範例下載
GitHub 連結
相關學習文章
如果你在學習上有不懂的地方,需要諮詢服務,可以參考站長服務,我想辨法解決你的問題
如果文章內容有過時、不適用或錯誤的地方,幫我在下方留言通知我一下,謝謝