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,可以重寫 OnActionExecuting 和 OnActionExecuted 方法,分別在操作方法執(zhí)行前后執(zhí)行自定義邏輯。例如,驗證輸入?yún)?shù)的有效性、記錄請求的執(zhí)行時間、處理異常等。
理清思路
要實現(xiàn)的功能
-
根據(jù)配置,校驗傳入的 Client ID 參數(shù)是否有效(參數(shù)名和參數(shù)所在位置都不確定,需要配置) -
校驗不通過的話返回錯誤信息 -
校驗通過的話,接口里需要能訪問到對應(yīng)的 Client 對象
如何實現(xiàn)?
首先是確定了這個功能是使用 Attribute 的形式來添加到接口的外邊,然后覆蓋 ActionFilterAttribute 的 OnActionExecutionAsync方法來實現(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 { get; set; } = "client_id";
/// <summary>
/// 設(shè)置驗證成功之后,存儲在 `HttpContext.Items` 對象中的 `Client` 對象的 key
/// </summary>
public string ClientItemKey { get; set; } = "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 { get; set; }
[Required]
[JsonPropertyName("password")]
public string Password { get; set; }
}
在接口中使用
[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<string> GetAuthorizeUrl([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
