Asp-Net-Core开发笔记:使用ActionFilterAttribute实现非侵入式的参数校验

asp,net,core,actionfilterattribute · 浏览次数 : 0

小编点评

## ValidateClientAttribute 示例分析 **用途:** ValidateClientAttribute 用于验证客户端ID,并根据验证结果进行相应处理。 **实现:** 1. **创建 ValidateClientAttribute 类:** - 使用 `ActionFilterAttribute` 的继承来创建 `ValidateClientAttribute` 类。 - 重写 `OnActionExecutionAsync` 方法,实现客户端ID验证逻辑。 2. **设置参数信息:** - 在 `ParameterName` 中设置验证参数的名称(即 `client_id`)。 - 创建一个 `ClientIdSource` 的枚举,包含所有可能验证的参数来源。 3. **获取参数值:** - 在 `OnActionExecutionAsync` 方法中,根据 `source` 属性获取验证参数。 - 使用反射获取参数值,并将其存储在 `HttpContext.Items` 中。 4. **验证参数:** - 在验证过程中,检查 `client` 对象是否存在,并进行相应的处理。 - 如果验证失败,返回错误信息。 5. **存储验证结果:** - 如果验证成功,将 `client` 对象存储在 `HttpContext.Items` 中。 **示例:** ```csharp public class ValidateClientAttribute : ActionFilterAttribute { public enum ClientIdSource { Query, Body, Route, Header } public string ParameterName { get; set; } = "client_id"; public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) { // 获取参数值 var clientId = GetClientIdFromSource(context.HttpContext.Items, source); // 验证参数 if (clientId == null) { throw new ArgumentNullException(Parameter); } // 将验证结果存储在 HttpContext 中 context.HttpContext.Items["client"] = client; await next(); } private string GetClientIdFromSource(Dictionary contextItems, ClientIdSource source) { switch (source) { case ClientIdSource.Query: return contextItems["query"].ToString(); case ClientIdSource.Body: return contextItems["body"].ToString(); // 其他参数处理逻辑... default: return null; } } } ``` **注意:** * `OnActionExecutionAsync` 方法中使用了 `context.HttpContext.Items` 来存储验证结果,这种方式与传统 `HttpContext` 中的 `Session` 或 `Request` 中的 `Session` 相类似。 * `ValidateClientAttribute` 可以与其他验证属性一起使用,例如 `Required` 或 `MinimumLength`。

正文

前言

在现代应用开发中,确保API的安全性和可靠性至关重要。

面向切面编程(AOP)通过将横切关注点(如验证、日志记录、异常处理)与核心业务逻辑分离,极大地提升了代码的模块化和可维护性。

在ASP.NET Core中,利用ActionFilterAttribute可以方便地实现AOP的理念,能够以简洁、高效的方式进行自定义验证。

本文将分享如何通过创建ValidateClientAttribute来验证客户端ID,并探讨这种方法如何体现AOP的诸多优势。

使用场景

本文使用场景是在我之前开发的单点认证项目中,当时的项目名称是 IdentityServerLite ,作为参考 IdentityServer4 设计的一个轻量级单点认证解决方案,不过我做得还不是很完善,而且属于是边学习 OAuth2.0 和 OpenID Connect 边做的,代码比较乱,关于这个单点认证项目,我后续可能会写一篇文章单独介绍,并且目前有一个重构后开源的计划。

在单点认证项目中,像登录、获取 AccessToken 、请求 Token 等操作都需要验证用户传入的 Client ID 参数是否有效,这部分逻辑是有些重复的,于是我就像使用一种更高效的方式来实现这个功能。

正好上次使用 AOP 的思想来实现非侵入性的审计日志功能,这次同样利用这种思想来实现这个校验功能。

ActionFilterAttribute

我发现之前那俩篇关于审计日志的实现文章没有怎么介绍这个东西

回顾一下:

现在再赘述一下~

ActionFilterAttribute 是 ASP.NET Core 提供的一个方便工具,用于在控制器的操作方法执行之前或之后添加自定义逻辑。这种机制使得我们可以在不改变操作方法本身的情况下,插入额外的处理逻辑,如验证、日志记录、异常处理等。这种特性体现了面向切面编程(AOP)的理念,能够有效地分离关注点,提高代码的模块化和可维护性。

通过继承 ActionFilterAttribute,可以重写 OnActionExecutingOnActionExecuted 方法,分别在操作方法执行前后执行自定义逻辑。例如,验证输入参数的有效性、记录请求的执行时间、处理异常等。

理清思路

要实现的功能

  • 根据配置,校验传入的 Client ID 参数是否有效(参数名和参数所在位置都不确定,需要配置)
  • 校验不通过的话返回错误信息
  • 校验通过的话,接口里需要能访问到对应的 Client 对象

如何实现?

首先是确定了这个功能是使用 Attribute 的形式来添加到接口的外边,然后覆盖 ActionFilterAttributeOnActionExecutionAsync方法来实现具体的校验逻辑。

之后还需要把从数据库里查找到的 Client 对象保存到 HttpContext 里,方便接口中使用这个对象。

HttpContext 是 ASP.NET Core 中用于封装 HTTP 请求和响应的对象。它提供了一种访问 HTTP 特定信息的统一方式,包括请求的详细信息、响应的内容、用户信息、会话数据、请求头和响应头等。每次 HTTP 请求对应一个 HttpContext 实例,该实例贯穿请求处理的整个生命周期。

这里我们利用 HttpContext 提供的 Items 这个键值对集合(用于在请求的不同中间件和组件之间共享数据)来共享 Client 对象。

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

开始写代码

ClientIdSource Enum

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

定义一个枚举

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

ValidateClientAttribute 实现

Filters 目录中创建 ValidateClientAttribute.cs 文件

根据配置,从指定的位置根据指定的参数名称读取 Client ID ,然后在数据库中查询。

public class ValidateClientAttribute(
  ClientIdSource source = ClientIdSource.Query
) : ActionFilterAttribute {
  /// <summary>
  /// 客户端ID的参数名称,注意是 DTO 里的属性名称,不是请求体JSON的字段名
  /// </summary>
  public string ParameterName { get; set; } = "client_id";

  /// <summary>
  /// 设置验证成功之后,存储在 `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 方法的第一个参数,通常是请求体中的 JSON 数据模型绑定转换为对应 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 的参数

其他几个参数位置还好,获取都比较容易

如果是 POST 或者 PUT 方法,一般都是把数据以 JSON 的形式放在 Request Body 里

这个时候,我们可以去读取这个 Body 的值,但读取完之后得自己解析 JSON,还得把 Stream 写回去,有点麻烦。而且如果 Body 是 XML 形式,还要用其他的解析方式。

这里我使用了反射的方式,让 AspNetCore 框架去处理这个 Request Body ,然后我直接用反射,根据参数名去读取 Client ID

使用

这是几个使用例子

参数在 Body 里

然后 DTO 里的参数名是 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) {

}

参数在 Query Params 里

参数名称是 client_id

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

参考资料

与Asp-Net-Core开发笔记:使用ActionFilterAttribute实现非侵入式的参数校验相似的内容:

Asp-Net-Core开发笔记:使用ActionFilterAttribute实现非侵入式的参数校验

前言 在现代应用开发中,确保API的安全性和可靠性至关重要。 面向切面编程(AOP)通过将横切关注点(如验证、日志记录、异常处理)与核心业务逻辑分离,极大地提升了代码的模块化和可维护性。 在ASP.NET Core中,利用ActionFilterAttribute可以方便地实现AOP的理念,能够以简

Asp-Net-Core开发笔记:使用原生的接口限流功能

前言 之前介绍过使用 AspNetCoreRateLimit 组件来实现接口限流 从 .Net7 开始,AspNetCore 开始内置限流组件,当时我们的项目还在 .Net6 所以只能用第三方的 现在都升级到 .Net8 了,当然是得来试试这个原生组件 体验后:配置使用都比较简单,不过功能也没有 A

Asp-Net-Core开发笔记:使用RateLimit中间件实现接口限流

前言 最近一直在忙(2月份沉迷steam,3月开始工作各种忙),好久没更新博客了,不过也积累了一些,忙里偷闲记录一下。 这个需求是这样的,我之前做了个工单系统,现在要对登录、注册、发起工单这些功能做限流,不能让用户请求太频繁。 从 .Net7 开始,已经有内置的限流功能了,但目前我们的项目还在使用

Asp-Net-Core开发笔记:FrameworkDependent搭配docker部署

## 前言 之前我写过一篇使用 docker 部署 AspNetCore 应用的文章,这种方式搭配 CICD 非常方便, build 之后 push 到私有的 dockerhub ,在生产服务器上 pull 下来镜像就可以直接运行了。 然而,有时需要一种更传统的部署方式,比如在本地打包可执行文件之后

Asp-Net-Core开发笔记:EFCore统一实体和属性命名风格

前言 C# 编码规范中,类和属性都是大写驼峰命名风格(PascalCase / UpperCamelCase),而在数据库中我们往往使用小写蛇形命名(snake_case),在默认情况下,EFCore会把原始的类名和属性名直接映射到数据库,这不符合数据库的命名规范。 为了符合命名规范,而且也为了看起

Asp-Net-Core开发笔记:给SwaggerUI加上登录保护功能

前言 在 SwaggerUI 中加入登录验证,是我很早前就做过的,不过之前的做法总感觉有点硬编码,最近 .Net8 增加了一个新特性:调用 MapSwagger().RequireAuthorization 来保护 Swagger UI ,但官方的这个功能又像半成品一样,只能使用 postman c

Asp-Net-Core开发笔记:快速在已有项目中引入EFCore

前言 很多项目一开始选型的时候没有选择EFCore,不过EFCore确实好用,也许由于种种原因后面还是需要用到,这时候引入EFCore也很方便。 本文以 StarBlog 为例,StarBlog 目前使用的 ORM 是 FreeSQL ,引入 EFCore 对我来说最大的好处是支持多个数据库,如果是

Asp-Net-Core学习笔记:单元测试和集成测试

## 前言 我在使用 AspNetCore 的这段时间内,看了很多开源项目和博客,发现各种 .Net 体系的新技术很多人都有关注和使用,但却很少有人关注测试。 测试是软件生命周期中的一个非常重要的阶段,对于保证软件的可靠性具有极其重要的意义。在应用程序的开发过程中,为了确保它的功能与预期一致,必须对

Asp-Net-Core学习笔记:gRPC快速入门

## 前言 此前,我在做跨语言调用时,用的是 Facebook 的 Thrift,挺轻量的,还不错。 >Thrift是一种接口描述语言和二进制通讯协议,它被用来定义和创建跨语言的服务。它被当作一个远程过程调用(RPC)框架来使用,是由Facebook为“大规模跨语言服务开发”而开发的。它通过一个代码

Asp-Net-Core开发笔记:进一步实现非侵入性审计日志功能

前言 上次说了利用 AOP 思想实现了审计日志功能,不过有同学反馈还是无法实现完全无侵入,于是我又重构了一版新的。 回顾一下:Asp-Net-Core开发笔记:实现动态审计日志功能 现在已经可以实现对业务代码完全无侵入的审计日志了,在需要审计的接口上加上 [AuditLog] 特性,就可以记录这个接