<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Asp-Net-Core開發(fā)筆記:使用ActionFilterAttribute實現(xiàn)非侵入式的參數(shù)校驗

          共 7509字,需瀏覽 16分鐘

           ·

          2024-05-17 23:10

          前言

          在現(xiàn)代應(yīng)用開發(fā)中,確保API的安全性和可靠性至關(guān)重要。

          面向切面編程(AOP)通過將橫切關(guān)注點(如驗證、日志記錄、異常處理)與核心業(yè)務(wù)邏輯分離,極大地提升了代碼的模塊化和可維護性。

          在ASP.NET Core中,利用ActionFilterAttribute可以方便地實現(xiàn)AOP的理念,能夠以簡潔、高效的方式進行自定義驗證。

          本文將分享如何通過創(chuàng)建ValidateClientAttribute來驗證客戶端ID,并探討這種方法如何體現(xiàn)AOP的諸多優(yōu)勢。

          使用場景

          本文使用場景是在我之前開發(fā)的單點認證項目中,當時的項目名稱是 IdentityServerLite ,作為參考 IdentityServer4 設(shè)計的一個輕量級單點認證解決方案,不過我做得還不是很完善,而且屬于是邊學習 OAuth2.0 和 OpenID Connect 邊做的,代碼比較亂,關(guān)于這個單點認證項目,我后續(xù)可能會寫一篇文章單獨介紹,并且目前有一個重構(gòu)后開源的計劃。

          在單點認證項目中,像登錄、獲取 AccessToken 、請求 Token 等操作都需要驗證用戶傳入的 Client ID 參數(shù)是否有效,這部分邏輯是有些重復(fù)的,于是我就像使用一種更高效的方式來實現(xiàn)這個功能。

          正好上次使用 AOP 的思想來實現(xiàn)非侵入性的審計日志功能,這次同樣利用這種思想來實現(xiàn)這個校驗功能。

          ActionFilterAttribute

          我發(fā)現(xiàn)之前那倆篇關(guān)于審計日志的實現(xiàn)文章沒有怎么介紹這個東西

          回顧一下:

          現(xiàn)在再贅述一下~

          ActionFilterAttribute 是 ASP.NET Core 提供的一個方便工具,用于在控制器的操作方法執(zhí)行之前或之后添加自定義邏輯。這種機制使得我們可以在不改變操作方法本身的情況下,插入額外的處理邏輯,如驗證、日志記錄、異常處理等。這種特性體現(xiàn)了面向切面編程(AOP)的理念,能夠有效地分離關(guān)注點,提高代碼的模塊化和可維護性。

          通過繼承 ActionFilterAttribute,可以重寫 OnActionExecutingOnActionExecuted 方法,分別在操作方法執(zhí)行前后執(zhí)行自定義邏輯。例如,驗證輸入?yún)?shù)的有效性、記錄請求的執(zhí)行時間、處理異常等。

          理清思路

          要實現(xiàn)的功能

          • 根據(jù)配置,校驗傳入的 Client ID 參數(shù)是否有效(參數(shù)名和參數(shù)所在位置都不確定,需要配置)
          • 校驗不通過的話返回錯誤信息
          • 校驗通過的話,接口里需要能訪問到對應(yīng)的 Client 對象

          如何實現(xiàn)?

          首先是確定了這個功能是使用 Attribute 的形式來添加到接口的外邊,然后覆蓋 ActionFilterAttributeOnActionExecutionAsync方法來實現(xiàn)具體的校驗邏輯。

          之后還需要把從數(shù)據(jù)庫里查找到的 Client 對象保存到 HttpContext 里,方便接口中使用這個對象。

          HttpContext 是 ASP.NET Core 中用于封裝 HTTP 請求和響應(yīng)的對象。它提供了一種訪問 HTTP 特定信息的統(tǒng)一方式,包括請求的詳細信息、響應(yīng)的內(nèi)容、用戶信息、會話數(shù)據(jù)、請求頭和響應(yīng)頭等。每次 HTTP 請求對應(yīng)一個 HttpContext 實例,該實例貫穿請求處理的整個生命周期。

          這里我們利用 HttpContext 提供的 Items 這個鍵值對集合(用于在請求的不同中間件和組件之間共享數(shù)據(jù))來共享 Client 對象。

          var client = HttpContext.Items["client"as Client;

          開始寫代碼

          ClientIdSource Enum

          Client ID 所在的位置不確定,需要在使用的時候配置

          定義一個枚舉

          public enum ClientIdSource {
            Query,
            Body,
            Route,
            Header
          }

          ValidateClientAttribute 實現(xiàn)

          Filters 目錄中創(chuàng)建 ValidateClientAttribute.cs 文件

          根據(jù)配置,從指定的位置根據(jù)指定的參數(shù)名稱讀取 Client ID ,然后在數(shù)據(jù)庫中查詢。

          public class ValidateClientAttribute(
            ClientIdSource source = ClientIdSource.Query
          ) : ActionFilterAttribute
           {
            /// <summary>
            /// 客戶端ID的參數(shù)名稱,注意是 DTO 里的屬性名稱,不是請求體JSON的字段名
            /// </summary>
            public string ParameterName { getset; } = "client_id";

            /// <summary>
            /// 設(shè)置驗證成功之后,存儲在 `HttpContext.Items` 對象中的 `Client` 對象的 key
            /// </summary>
            public string ClientItemKey { getset; } = "client";

            public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) {
              var clientId = "";

              switch (source) {
                case ClientIdSource.Query:
                  clientId = context.HttpContext.Request.Query[ParameterName];
                  break;
                case ClientIdSource.Body:
                  // 使用反射從請求體中讀取 client_id
                  // 這里讀取到的 body 是 Controller 下 Action 方法的第一個參數(shù),通常是請求體中的 JSON 數(shù)據(jù)模型綁定轉(zhuǎn)換為對應(yīng) DTO 實例
                  var body = context.ActionArguments.Values.FirstOrDefault();
                  if (body != null) {
                    var clientProp = body.GetType().GetProperty(ParameterName);
                    if (clientProp != null) {
                      clientId = clientProp.GetValue(body) as string;
                    }
                  }
                  break;
                case ClientIdSource.Route:
                  clientId = context.RouteData.Values[ParameterName] as string;
                  break;
                case ClientIdSource.Header:
                  clientId = context.HttpContext.Request.Headers[ParameterName];
                  break;
              }

              if (string.IsNullOrWhiteSpace(clientId)) {
                throw new ArgumentNullException(ParameterName);
              }

              var clientRepo = context.HttpContext.RequestServices.GetRequiredService<IBaseRepository<Client>>();
              var client = await clientRepo.Select.Where(a => a.ClientId == clientId).FirstAsync();

              if (client != null) {
                context.HttpContext.Items["client"] = client;
                await next();
              }
              else {
                context.Result = new NotFoundObjectResult(
                  new ApiResponse { Message = $"client with id {clientId} not found" });
              }
            }
          }

          有幾點需要注意的,下面介紹一下

          通過反射獲取 request body 的參數(shù)

          其他幾個參數(shù)位置還好,獲取都比較容易

          如果是 POST 或者 PUT 方法,一般都是把數(shù)據(jù)以 JSON 的形式放在 Request Body 里

          這個時候,我們可以去讀取這個 Body 的值,但讀取完之后得自己解析 JSON,還得把 Stream 寫回去,有點麻煩。而且如果 Body 是 XML 形式,還要用其他的解析方式。

          這里我使用了反射的方式,讓 AspNetCore 框架去處理這個 Request Body ,然后我直接用反射,根據(jù)參數(shù)名去讀取 Client ID

          使用

          這是幾個使用例子

          參數(shù)在 Body 里

          然后 DTO 里的參數(shù)名是 ClientId

          public class PwdLoginDto : LoginDto {
            [Required]
            [JsonPropertyName("username")]
            public string Username { getset; }

            [Required]
            [JsonPropertyName("password")]
            public string Password { getset; }
          }

          在接口中使用

          [HttpPost("login/password")]
          [ValidateClient(ClientIdSource.Body, ParameterName = "ClientId")]
          public async Task<IActionResult> LoginByPassword(PwdLoginDto dto) {

          }

          參數(shù)在 Query Params 里

          參數(shù)名稱是 client_id

          [HttpGet("authorize/url")]
          [ValidateClient(ClientIdSource.Query, ParameterName = "client_id")]
          public ApiResponse<stringGetAuthorizeUrl([FromQuery] AuthorizeInput input) {
            return new ApiResponse<string>(GenerateAuthorizeUrl(input));
          }

          參考資料

          • https://learn.microsoft.com/en-us/aspnet/core/mvc/controllers/filters
          • https://learn.microsoft.com/en-us/aspnet/core/fundamentals/http-context


          瀏覽 32
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  色屁屁TS人妖系列二区 | 精品av国产日韩一区二区 | jiujiuav | 亚洲操操操 | 无码在线播放观看 |