只有累積,沒有奇蹟

2022年4月28日 星期四

[WEB API] Swagger - 在 Headers 中新增 API Token 驗證

問題
在開發 API 時都會在網站加上 API Token 機制,當收到一個 Request 請求時 API 會驗證 Token 的正確性,確認請求參數中的 Token 是否是有效 / 已授權 / 有沒有過期或是用來當 SSO (Single sign-on) 的使用,驗證無誤後才會進入接口的邏輯處理目前公司內部 API 專案也有驗證 Token 的設計之前文章介紹了 Swagger 基本使用 發現在線上文件進行 API 測試沒有 Token 的話根本無法測試,此篇記錄如果遇到此問題時該如何解決

解決方案
Swagger 在產生 API 接口會將參數 (Model) 資訊顯示在 html 文件,但如果驗證 Token 機制是在 Header時所生成的 Web API 文件就不會生成在網頁上,寫了一個簡單的範例專案讓大家比較好了解 (僅示意,相信寫法上有更多好的驗證機制)如下圖Code 內容所示,程式碼在進入時就檢查 Request Header 是否有提供 Token 參數,此時使用 Swagger 就是沒有地方可以輸入 Token 參數,因此測試就會因為不符合 Token 驗證會一直爆炸
 
該如何解決呢? 在 Swashbuckle 是很容易擴充的,可以透過加入 IOperationFilter 達到在線上文件新增驗證 Header 的需求,更符合測試上的情境,以下為研究後的小小心得

新增類別並實作 IOperationFilter 
以範例程式為例新增 HeaderTokenOperationFilter 類別並實作 IOperationFilter 介面中的方法 Apply
public class HeaderTokenOperationFilter : IOperationFilter
{
    public void Apply(Operation operation, SchemaRegistry schemaRegistry, ApiDescription apiDescription)
    {
 if (operation.parameters == null)
  operation.parameters = new List();

 operation.parameters.Add(new Parameter
 {
  name = "Token",
  @in = "header", //query
  type = "string",
  description = "User Token In Header",
  required = true
 });
    }
}

程式說明 : 
  • Line 10 : 要加入的 Parameter 名稱
  • Line 11 : 請求時所要放的位置,此範例是在 Header,也可以放在 QueryString
  • Line 12 : 參數型態
  • Line 13 : Parameter 說明
  • Line 14 : 是否必填
設定 SwaggerConfig
開啟 SwaggerConfig.cs 檔案加入下列程式
驗證
重新開啟專案,發現 Swagger 已經有 Token 參數可以提供輸入並會顯示為必填欄位
為了驗證是否真的有在 Request 的 Headers 帶 Token 參數,可以透過 Chrome F12 來觀察是否有帶正確的 Token 參數 : F12 > Network > Headers 
驗證無誤,打完收工

完整 Sample
完整的 Code 有需要請自行服用
    public class HomeController : ApiController
    {
        public string _userSsoToken = "1aa2a116-876e-464b-bdaf-d6d3adaeb4e4";
        
        /// 
        /// 取得使用者金額
        /// 
        /// 
        [HttpPost]
        [Route("GetMoney")]
        [SwaggerRequestExample(typeof(MemberUser), typeof(MemberUserExample))]
        public IHttpActionResult GetMoney(MemberUser input)
        {
            var userInfo = new UserInfo();
            
            // 檢查 Header 是否有 token
            if (Request.Headers.Contains("Token"))
            {
                string token = Request.Headers.GetValues("Token").First();

                if (token == _userSsoToken)
                {
                    // 驗證無誤 回傳資料
                    if (input.UserId == 9487)
                    {
                        GetUserInfo(userInfo);
                    }
                }
                else
                {
                    // 驗證錯誤
                    TokenError(userInfo);
                }

                userInfo.Token = token;
            }
            
            return Ok(userInfo);
        }

        private static void TokenError(UserInfo userInfo)
        {
            userInfo.Code = "999";
            userInfo.Message = "Token error";
        }

        private static void GetUserInfo(UserInfo userInfo)
        {
            userInfo.Code = "000";
            userInfo.Message = "Success";
            userInfo.UserId = "9487";
            userInfo.Name = "marcus";
            userInfo.Money = "1000";
        }
    }
    
    internal class UserInfo
    {
        public string UserId { get; set; }
        public string Name { get; set; }
        public string Money { get; set; }
        public string Code { get; set; }
        public string Message { get; set; }
        public string Token { get; set; }
    }

    public class MemberUser
    {
        public int UserId { get; set; }
    }

    public class MemberUserExample : IExamplesProvider
    {
        public object GetExamples()
        {
            return new MemberUser
            {
                UserId = 9487,
            };
        }
    }
    public class HeaderTokenOperationFilter : IOperationFilter
    {
        public void Apply(Operation operation, SchemaRegistry schemaRegistry, ApiDescription apiDescription)
        {
            if (operation.parameters == null)
                operation.parameters = new List();

            operation.parameters.Add(new Parameter
            {
                name = "Token",
                @in = "header",
                type = "string",
                description = "User Token In Header",
                required = true
            });
        }
    }
    參考
    add-an-authorization-header-to-your-swagger-ui-with-swashbuckle
    .NET -Swagger Web API for Basic Authentication

    2022年4月10日 星期日

    [WEBAPI] Swagger - 用 Swashbuckle.Examples 加上有意義的測試數據

    問題
    Swagger 是一個可以將 WebAPI 快速文件化的套件,產生出來的線上文件除了可以列出 API 詳細資料外還可以直接在網頁上進行測試的動作,對開發者和接 API 的使用者來說十分方便上一篇文章介紹了 Swagger 基本使用 說明最近想要在公司內部推廣使用 Swagger 服務,資深同事提到過去有陣子曾經使用過 Swagger 服務但  每次要使用 API 接口服務時參數 (params) 資訊都要重新輸入 ,有點麻煩用一陣子之後大家就回去使用 Postman 了,了解後發現的確在測試時會花時間在輸入測試資料,如果測試環境測試資料都是固定的,就可省下輸入資料的時間更可避免 key 錯資料的狀況發生 ( 參考下方 gif 檔案)這邊文章記錄解決此問題的過程
    解決方案
    Swagger 在產生 API 接口的 Model 時取得該物件的 properties 及其對應型別,將其物件資訊顯示在 html文件但不會生成 "可以測試的真實數據 (或是你想要的)" 資料,也就是說替換下圖的紅色框框資料,讓它是有意義的資料搜尋後發現可以使用 Swashbuckle.Examples 來滿足我們小小的需求以下整理研究後的步驟與使用說明

    安裝 Swashbuckle.Examples
    Step 1. 開啟 nuget > 輸入 Swashbuckle.Examples 並下載安裝
    Step 2. 目前最新版是 3.10.0 且不會額外下載其他特別的 dll > 下一步
    Step 3. 可以到專案參考是否有 Swashbuckle.Examples dll,有的話就是下載完成

    實作 Request Sample
    接下來在 Method 上加上 SwaggerRequestExample Attribute第一個參數是原本的 Model第二個參數是範例 Model 名稱以範例專案的例子來說Login 接口第一個參數為 User (原本的 Model) 第二個參數 UserExample (新 Model)
    [SwaggerRequestExample(typeof(Users), typeof(UserExample))]
    public IHttpActionResult Login(Users input) 
    接者新增 UserExample 類別並實作 IExamplesProvider 介面,並將我們指定的測試資料定義在該介面的 GetExample 方法中
    public class UserExample : IExamplesProvider
    {
        public object GetExamples()
        {
     return new Users
     {
      Account = "marcus",
      Password = "123456",
      Key = 9487,
     };
        }
    }
    
    完整的 Code 請參考
    public class HomeController : ApiController
    {   
        [Route("Login")]
        [SwaggerRequestExample(typeof(Users), typeof(UserExample))]
        public IHttpActionResult Login(Users input)
        {
         var rep = new ResponseObject();
    
     if (input.Account == "marcus" 
      && input.Password == "123456" 
      && input.Key == 9487)
     {
      rep.Code = "000";
      rep.Message = "Success";
      rep.Token = Guid.NewGuid();
     }
     else
     {
      rep.Code = "999";
      rep.Message = "Please check your input params";
     }
    
     return Ok(rep);
        }
    }
    
    public class Users
    {
        public string Password { get; set; }
        public string Account { get; set; }
        public int Key { get; set; }
    }
    
    public class UserExample : IExamplesProvider
    {
        public object GetExamples()
        {
     return new Users
     {
      Account = "marcus",
      Password = "123456",
      Key = 9487,
     };
        }
    }
    
    public class ResponseObject
    {
        public string Code { get; set; }
        public string Message { get; set; }
        public Guid Token { get; set; }
    }
    
    設定 SwaggerConfig
    安裝完 Swagger.example package,在 Code 指定好需要產生的 example 物件,下一步是要在 Swagger 設定 OperationFilter開啟 SwaggerConfig.cs 檔案加入下列程式

    打完收工 
    透過以上設定,在重新開啟 Swagger 就可以發現 example model 已生效,省下了輸入 API 參數的時間可以更專注的在測試 / 驗證接口正確性,提升效率早點下班回家為了證明哥沒有在唬爛調整後的畫面如下
    Swashbuckle.Examples 在使用上簡單容易上手,但除了可以透過此套件定義 Example Request Model 之外,官網文件上與有提到可以設定 Example Response Model、Description、Authorization...等更多資訊,也支持 .NET Core 版本,如果有需要各位大大也可以自行研究看看
    Summary
    這問題聽到當下嚇到吃手手,江湖上常聽到攻城屍很懶惰但沒想到懶到這程度,但仔細了解後發現問題確實存在,且驗證後發現,在此範例三個參數使用完整整省去一半的時間 18 sec > 9sec想到之前參加研討會講師所講的,懶是一個美德否則會局限自己的成長

    參考
    Swashbuckle.Examples
    Swashbuckle Pro Tips for ASP.NET Web API – Example(s) Using AutoFixture
    Swagger for Web API Document – Part Ⅱ

    2022年4月4日 星期一

    [Azure] App Service Diagnostics - Auto-Heal

    前言
    上一篇提到了如何在 Azure 取得當下的 memory dump 資訊 App Service Diagnostics - 應用服務診斷,這一篇則是透過另外一種方式使用 Auto-heal 的方式設定 memory 達到一定的水位時,觸發自動收集 memory 的使用狀況,並在自訂的條件下抓取 memory 資料,並指定 dump file 放置在某個 storage account 帳號中。若對於上述內容有問題或是不清楚的地方,歡迎提出來一起討論

    Auto-Heal
    今天要分享的是透過 Auto-Heal 方式取得應用程式發生例外或是異常時,透過設定特定的行動、內存限制定義各種條件,採取什麼特定的行動方案,例如重啟應用程式、紀錄事件或是啟動另外一個可執行方案,以下就來介紹如何使用 Azure 來設定 Auto-Heal 選項
    Step 1 : 開啟 App Service
    Step 2 : 點選左邊清單的 Diagnose and solve problems 功能
    Step 3 : 在右邊框輸入 "點選 Auto-heal"
    Step 4 : Custom Auto-Heal Rules Enabled 選擇開啟 (on)
    Step 5 : 在 Define Conditions 中選擇 Memory limit,並設置水位值為 (5872025),此設定值可以依據實體狀況做調整,是指以 KB 為單位的 App 占用 momory 的值
    Step 6 : 在 Configure Actions 選 Custom Action,下方則選擇 Memory dump
    Step 7 : Tool options 選擇 collectLogs
    Step 8 : 選擇按下 Select,選擇要儲存的 storage 位置
    Step 9 : 點擊儲存 Auto-heal 設定配置,此功能將自動監控應用程式的 Memory 使用情況,並在剛剛自定義的條件下取得應用程式的 Dump files

    透過以上詳細的資訊,就可以發生異常問題的 dump file 上傳給微軟或者是有經驗的 SRE 團隊分析與使用;另外,如果一陣子發現 storage account 都沒有異常的 memory dump file 的話,可以試著調降 memory 水位值,這樣更有機會可以取得所需要的資訊內容,才有機會找到蛛絲馬跡再進行下一步的推斷。

    結論
    如果想要了解更多細節可以參考 Announcing the New Auto Healing Experience in App Service Diagnostics 以及 Azure App Service tips: Increase reliability with the Auto-Healing features 這兩篇文章內容都有詳細說明 Auto-heal 的用法與步驟,Hope it helps :D

    Copyright © m@rcus 學習筆記 | Powered by Blogger

    Design by Anders Noren | Blogger Theme by NewBloggerThemes.com