前言

在现代应用开发中,确保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实现非侵入式的参数校验的更多相关文章

  1. 在CentOS7 开发与部署 asp.net core app笔记

    原文:在CentOS7 开发与部署 asp.net core app笔记 版权声明:本文为博主原创文章,未经博主允许不得转载. https://blog.csdn.net/lihongzhai/art ...

  2. 2月送书福利:ASP.NET Core开发实战

    大家都知道我有一个公众号“恰童鞋骚年”,在公众号2020年第一天发布的推文<2020年,请让我重新介绍我自己>中,我曾说到我会在2020年中每个月为所有关注“恰童鞋骚年”公众号的童鞋们送一 ...

  3. [转]ASP.NET Core 开发-Logging 使用NLog 写日志文件

    本文转自:http://www.cnblogs.com/Leo_wl/p/5561812.html ASP.NET Core 开发-Logging 使用NLog 写日志文件. NLog 可以适用于 . ...

  4. ASP.NET Core 开发-中间件(Middleware)

    ASP.NET Core开发,开发并使用中间件(Middleware). 中间件是被组装成一个应用程序管道来处理请求和响应的软件组件. 每个组件选择是否传递给管道中的下一个组件的请求,并能之前和下一组 ...

  5. ASP.NET Core开发-Docker部署运行

    ASP.NET Core开发Docker部署,.NET Core支持Docker 部署运行.我们将ASP.NET Core 部署在Docker 上运行. 大家可能都见识过Docker ,今天我们就详细 ...

  6. ASP.NET Core开发-后台任务利器Hangfire使用

    ASP.NET Core开发系列之后台任务利器Hangfire 使用. Hangfire 是一款强大的.NET开源后台任务利器,无需Windows服务/任务计划程序. 可以使用于ASP.NET 应用也 ...

  7. ASP.NET Core开发-读取配置文件Configuration

    ASP.NET Core 是如何读取配置文件,今天我们来学习. ASP.NET Core的配置系统已经和之前版本的ASP.NET有所不同了,之前是依赖于System.Configuration和XML ...

  8. ASP.NET Core 开发-Entity Framework (EF) Core 1.0 Database First

    ASP.NET Core 开发-Entity Framework Core 1.0 Database First,ASP.NET Core 1.0 EF Core操作数据库. Entity Frame ...

  9. ASP.NET Core 开发-Logging 使用NLog 写日志文件

    ASP.NET Core 开发-Logging 使用NLog 写日志文件. NLog 可以适用于 .NET Core 和 ASP.NET Core . ASP.NET Core已经内置了日志支持,可以 ...

  10. ASP.NET Core 开发-中间件(StaticFiles)使用

    ASP.NET Core 开发,中间件(StaticFiles)的使用,我们开发一款简易的静态文件服务器. 告别需要使用文件,又需要安装一个web服务器.现在随时随地打开程序即可使用,跨平台,方便快捷 ...

随机推荐

  1. #矩阵树定理,高斯消元#洛谷 4111 [HEOI2015]小 Z 的房间

    题目 分析 题目要求生成树个数,求出基尔霍夫矩阵后高斯消元, 但是这里模数不是质数,所以要辗转相除法 代码 #include <cstdio> #include <cctype> ...

  2. Java处理关键字进行脱敏操作

    1.通过表头获取需要处理的下标列 注:此处导出表格时对关键字进行脱敏处理 /** * . * 对表头进行过滤判断 * * @param headers 表头 * @return 对应的下标列及方法名 ...

  3. 部署javaweb项目到阿里云ecs(centos7)

    阿里云文档 https://help.aliyun.com/document_detail/51376.html?spm=5176.12901015.0.i12901015.af8f525cCPi8Q ...

  4. openGauss Gin 索引

    openGauss Gin 索引 概述 GIN(Generalized Inverted Index)通用倒排索引,是首选的文本搜索索引类型.倒排索引对应的列上的数据类型通常是一个多值类型,索引中包含 ...

  5. openGauss单机部署

    openGauss 单机部署 一.安装环境 操作系统:虚拟机 VMware.CentOS7.9 环境设置: (1)虚拟机内存 3G.磁盘 100G (2)系统版本修改 一开始使用了 centos8,无 ...

  6. 面试连环炮系列(二十六):什么情况下JVM频繁发生full GC

    1. 什么情况下JVM频繁发生full GC? full gc触发条件是老年代空间不足,具体原因有四个: 系统并发高.执行耗时长或者创建对象过多,导致 young gc频繁,且gc后存活对象太多,但是 ...

  7. 重学c#系列——linq(4) [三十]

    前言 简单介绍一下linq 查询表达式. 正文 上文其实已经介绍了查询表达式了. 但是呢,这里就介绍一些复杂一点的. 这里不会去介绍查询表达式,而是直接介绍一些复杂的. let 字句. static ...

  8. TypeScript 中接口的理解?应用场景?

    一.是什么 「接口」是一系列抽象方法的声明,是一些方法特征的集合,这些方法都应该是抽象的,需要由具体的「类」去实现,然后第三方就可以通过这组抽象方法调用,让具体的类执行具体的方法 简单来讲,一个接口所 ...

  9. 技术解读:现代化工具链在大规模 C++ 项目中的运用 | 龙蜥技术

    简介: 本文详细介绍我们在实际工作中的大型 C++ 项目中现代化工具链的实践以及结果. 编者按:C++ 语言与编译器一直都在持续演进,出现了许多令人振奋的新特性,同时还有许多新特性在孵化阶.除此之外, ...

  10. 那些你不知道的TCP冷门知识!

    简介: 最近在做数据库相关的事情,碰到了很多TCP相关的问题,新的场景新的挑战,有很多之前并没有掌握透彻的点,大大开了一把眼界,选了几个案例分享一下. 最近在做数据库相关的事情,碰到了很多TCP相关的 ...