利用中间件+特性解决minimal api 项目拦截功能

IRC_NewDev
hang 2024-10-25 11:02:10 +08:00
parent 5023f5ed31
commit 77a11cacc2
6 changed files with 382 additions and 21 deletions

View File

@ -102,7 +102,7 @@ builder.Services.AddControllers(options =>
// Panda动态WebApi + UnifiedApiResultFilter + 省掉控制器代码
builder.Services.AddDynamicWebApiSetup();
//MinimalAPI
builder.Services.AddMasaMinimalAPIs();
builder.Services.AddMasaMinimalAPiSetUp();
//AutoMapper
builder.Services.AddAutoMapperSetup();

View File

@ -1,4 +1,5 @@
using IRaCIS.Core.Application.Service.BusinessFilter;
using IRaCIS.Core.Application.BusinessFilter;
using IRaCIS.Core.Application.Service.BusinessFilter;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
@ -44,6 +45,7 @@ namespace IRaCIS.Core.API
options.RouteHandlerBuilder = t => {
t.RequireAuthorization()
.AddEndpointFilter<LimitUserRequestAuthorizationEndpointFilter>()
.AddEndpointFilter<TrialGlobalLimitEndpointFilter>()
//.AddEndpointFilter<ModelValidationEndpointFilter>()
.AddEndpointFilter<UnifiedApiResultEndpointFilter>()
.WithGroupName("Institution").DisableAntiforgery();

View File

@ -0,0 +1,184 @@
using IRaCIS.Core.Application.BusinessFilter;
using IRaCIS.Core.Application.Helper;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using static IRaCIS.Core.Domain.Share.StaticData;
namespace IRaCIS.Core.Application.Filter;
public class TrialGlobalLimitActionFilter(IFusionCache _fusionCache, IUserInfo _userInfo, IRepository<Trial> _trialRepository) : IAsyncActionFilter
{
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
// 检查当前 endpoint 是否包含 TrialGlobalLimitAttribute 特性
var trialLimitAttribute = context.HttpContext.GetEndpoint()?.Metadata.GetMetadata<TrialGlobalLimitAttribute>();
if (trialLimitAttribute != null)
{
var optActions = trialLimitAttribute._optActions;
// 这里开始执行原有的逻辑
var requestHost = context.HttpContext.Request.Host;
// 检查请求是否来自 localhost:6100
if (requestHost.Host == "localhost" && (requestHost.Port == 6100 || requestHost.Port == 3305))
{
await next();
return;
}
#region 特殊用户类型拦截
// 用户类型检查
if (_userInfo.UserTypeEnumInt == (int)UserTypeEnum.CRA && _userInfo.RequestUrl.ToLower() != "TrialDocument/userConfirm".ToLower())
{
//对不起,您的账户没有操作权限
context.Result = new JsonResult(ResponseOutput.NotOk(I18n.T("TrialResource_NoAccessPermission")));
return;
}
#endregion
#region 从多种途径取TrialId
//TrialId 传递的途径多种可能在path 可能在body 可能在数组中也可能在对象中可能就在url
var trialIdStr = string.Empty;
if (!string.IsNullOrWhiteSpace(context.HttpContext.Request.Query["trialId"]))
{
trialIdStr = context.HttpContext.Request.Query["trialId"];
}
//先尝试从path中取TrialId
else if (context.HttpContext.Request.RouteValues.Keys.Any(t => t.Contains("trialId")))
{
var index = context.HttpContext.Request.RouteValues.Keys.ToList().IndexOf("trialId");
trialIdStr = context.HttpContext.Request.RouteValues.Values.ToList()[index] as string;
}
else if (context.HttpContext.Request.Headers["Referer"].ToString().Contains("trialId"))
{
var headerStr = context.HttpContext.Request.Headers["Referer"].ToString();
var trialIdIndex = headerStr.IndexOf("trialId");
var matchResult = Regex.Match(headerStr.Substring(trialIdIndex), @"[0-9a-fA-F]{8}(-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}");
if (matchResult.Success)
{
trialIdStr = matchResult.Value;
}
else
{
//---正则取请求Refer 中trialId 失败,请联系开发人员核查
context.Result = new JsonResult(ResponseOutput.NotOk(I18n.T("TrialResource_ReferTrialIdFailed")));
return;
}
}
else
{
#region body 中取数据
//设置可以多次读
context.HttpContext.Request.EnableBuffering();
var reader = new StreamReader(context.HttpContext.Request.Body);
var contentFromBody = await reader.ReadToEndAsync();
//读取后,流的位置还原
context.HttpContext.Request.Body.Seek(0, SeekOrigin.Begin);
//context.HttpContext.Request.Body.Position = 0;
//找到参数位置在字符串中的索引
var trialIdIndex = contentFromBody.IndexOf("\"TrialId\"", StringComparison.OrdinalIgnoreCase);
if (trialIdIndex > -1)
{
// (?<="trialId" *: *").*?(?=",)
//使用正则 [0-9a-fA-F]{8}(-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}
var matchResult = Regex.Match(contentFromBody.Substring(trialIdIndex), @"[0-9a-fA-F]{8}(-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}");
if (matchResult.Success)
{
//有可能匹配错误 "trialId":"","documentId":"b8180000-3e2c-0016-9fe0-08da33f96236" 从缓存里面验证下
trialIdStr = matchResult.Value;
var trialStatusStr = await _fusionCache.GetOrSetAsync(CacheKeys.Trial(trialIdStr), _ => CacheHelper.GetTrialStatusAsync(Guid.Parse(trialIdStr), _trialRepository), TimeSpan.FromDays(7));
if (string.IsNullOrWhiteSpace(trialStatusStr))
{
//数据库 检查该项目Id不对
context.Result = new JsonResult(ResponseOutput.NotOk(I18n.T("TrialResource_ReferTrialIdFailed")));
return;
}
}
else
{
//---正则取请求Refer 中trialId 失败,请联系开发人员核查
context.Result = new JsonResult(ResponseOutput.NotOk(I18n.T("TrialResource_ReferTrialIdFailed")));
return;
}
//使用字符串取 如果是swagger 可能有时取的不对 因为空格的原因
//trialIdStr = contentFromBody.Substring(trialIdIndex + "TrialId".Length + 4, 3
}
#endregion
}
#endregion
//通过path 或者body 找到trialId 了
if (!string.IsNullOrWhiteSpace(trialIdStr))
{
var trialStatusStr = await _fusionCache.GetOrSetAsync(CacheKeys.Trial(trialIdStr), _ => CacheHelper.GetTrialStatusAsync(Guid.Parse(trialIdStr), _trialRepository), TimeSpan.FromDays(7));
// 这里是统一拦截 项目有关的操作允许情况(特殊的地方比如项目配置有的在多种状态初始化ongoing都可以操作有的仅仅在Initializing还有 项目添加和更新,不走这里,特殊处理,不然在这里显得很乱,判断是哪个接口)
if (trialStatusStr == StaticData.TrialState.TrialOngoing || optActions.Any(t => t == TrialOpt.BeforeOngoingCantOpt))
{
await next();
}
// 项目停止、或者完成 不允许操作
else
{
//---本次请求被配置规则拦截:项目状态处于进行中时,才允许操作,若此处逻辑有误,请联系开发人员修改
context.Result = new JsonResult(ResponseOutput.NotOk(I18n.T("TrialResource_InterceptedProjectStatusRule")));
}
}
//添加项目 签名系统文档的时候 不做拦截 但是更新项目 签名项目文档的时候需要拦截
else if (optActions.Any(t => t == TrialOpt.AddOrUpdateTrial || t == TrialOpt.SignSystemDocNoTrialId))
{
await next();
}
else
{
//如果项目相关接口没有传递trialId 会来到这里,提醒,以便修改
//---该接口参数中,没有传递项目编号,请核对。
context.Result = new JsonResult(ResponseOutput.NotOk(I18n.T("TrialResource_MissingProjectNumber")));
}
}
// 如果没有 TrialGlobalLimitAttribute则直接执行后续逻辑
await next();
}
}

View File

@ -1,17 +0,0 @@
using Microsoft.AspNetCore.Http;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace IRaCIS.Core.Application.BusinessFilter;
public class TrialGlobalLimitEndpointFilter : IEndpointFilter
{
public ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next)
{
throw new NotImplementedException();
}
}

View File

@ -0,0 +1,190 @@
using IRaCIS.Core.Application.Helper;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using static IRaCIS.Core.Domain.Share.StaticData;
namespace IRaCIS.Core.Application.BusinessFilter;
public class TrialGlobalLimitAttribute : Attribute
{
public readonly string[] _optActions;
public TrialGlobalLimitAttribute(params string[] optActions)
{
_optActions = optActions;
}
}
public class TrialGlobalLimitEndpointFilter(IFusionCache _fusionCache, IUserInfo _userInfo, IRepository<Trial> _trialRepository) : IEndpointFilter
{
public async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next)
{
// 检查当前 endpoint 是否包含 TrialGlobalLimitAttribute 特性
var trialLimitAttribute = context.HttpContext.GetEndpoint()?.Metadata.GetMetadata<TrialGlobalLimitAttribute>();
if (trialLimitAttribute != null)
{
var optActions = trialLimitAttribute._optActions;
// 这里开始执行原有的逻辑
var requestHost = context.HttpContext.Request.Host;
// 检查请求是否来自 localhost:6100
if (requestHost.Host == "localhost" && (requestHost.Port == 6100 || requestHost.Port == 3305))
{
return await next(context);
}
#region 特殊用户类型拦截
// 用户类型检查
if (_userInfo.UserTypeEnumInt == (int)UserTypeEnum.CRA && _userInfo.RequestUrl.ToLower() != "TrialDocument/userConfirm".ToLower())
{
//对不起,您的账户没有操作权限
return Results.Json(ResponseOutput.NotOk(I18n.T("TrialResource_NoAccessPermission")));
}
#endregion
#region 从多种途径取TrialId
//TrialId 传递的途径多种可能在path 可能在body 可能在数组中也可能在对象中可能就在url
var trialIdStr = string.Empty;
if (!string.IsNullOrWhiteSpace(context.HttpContext.Request.Query["trialId"]))
{
trialIdStr = context.HttpContext.Request.Query["trialId"];
}
//先尝试从path中取TrialId
else if (context.HttpContext.Request.RouteValues.Keys.Any(t => t.Contains("trialId")))
{
var index = context.HttpContext.Request.RouteValues.Keys.ToList().IndexOf("trialId");
trialIdStr = context.HttpContext.Request.RouteValues.Values.ToList()[index] as string;
}
else if (context.HttpContext.Request.Headers["Referer"].ToString().Contains("trialId"))
{
var headerStr = context.HttpContext.Request.Headers["Referer"].ToString();
var trialIdIndex = headerStr.IndexOf("trialId");
var matchResult = Regex.Match(headerStr.Substring(trialIdIndex), @"[0-9a-fA-F]{8}(-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}");
if (matchResult.Success)
{
trialIdStr = matchResult.Value;
}
else
{
//---正则取请求Refer 中trialId 失败,请联系开发人员核查
return Results.Json(ResponseOutput.NotOk(I18n.T("TrialResource_ReferTrialIdFailed")));
}
}
else
{
#region body 中取数据
//设置可以多次读
context.HttpContext.Request.EnableBuffering();
var reader = new StreamReader(context.HttpContext.Request.Body);
var contentFromBody = await reader.ReadToEndAsync();
//读取后,流的位置还原
context.HttpContext.Request.Body.Seek(0, SeekOrigin.Begin);
//context.HttpContext.Request.Body.Position = 0;
//找到参数位置在字符串中的索引
var trialIdIndex = contentFromBody.IndexOf("\"TrialId\"", StringComparison.OrdinalIgnoreCase);
if (trialIdIndex > -1)
{
// (?<="trialId" *: *").*?(?=",)
//使用正则 [0-9a-fA-F]{8}(-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}
var matchResult = Regex.Match(contentFromBody.Substring(trialIdIndex), @"[0-9a-fA-F]{8}(-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}");
if (matchResult.Success)
{
//有可能匹配错误 "trialId":"","documentId":"b8180000-3e2c-0016-9fe0-08da33f96236" 从缓存里面验证下
trialIdStr = matchResult.Value;
var trialStatusStr = await _fusionCache.GetOrSetAsync(CacheKeys.Trial(trialIdStr), _ => CacheHelper.GetTrialStatusAsync(Guid.Parse(trialIdStr), _trialRepository), TimeSpan.FromDays(7));
if (string.IsNullOrWhiteSpace(trialStatusStr))
{
//数据库 检查该项目Id不对
return Results.Json(ResponseOutput.NotOk(I18n.T("TrialResource_ReferTrialIdFailed")));
}
}
else
{
//---正则取请求Refer 中trialId 失败,请联系开发人员核查
return Results.Json(ResponseOutput.NotOk(I18n.T("TrialResource_ReferTrialIdFailed")));
}
//使用字符串取 如果是swagger 可能有时取的不对 因为空格的原因
//trialIdStr = contentFromBody.Substring(trialIdIndex + "TrialId".Length + 4, 3
}
#endregion
}
#endregion
//通过path 或者body 找到trialId 了
if (!string.IsNullOrWhiteSpace(trialIdStr))
{
var trialStatusStr = await _fusionCache.GetOrSetAsync(CacheKeys.Trial(trialIdStr), _ => CacheHelper.GetTrialStatusAsync(Guid.Parse(trialIdStr), _trialRepository), TimeSpan.FromDays(7));
// 这里是统一拦截 项目有关的操作允许情况(特殊的地方比如项目配置有的在多种状态初始化ongoing都可以操作有的仅仅在Initializing还有 项目添加和更新,不走这里,特殊处理,不然在这里显得很乱,判断是哪个接口)
if (trialStatusStr == StaticData.TrialState.TrialOngoing || optActions.Any(t => t == TrialOpt.BeforeOngoingCantOpt))
{
return await next(context);
}
// 项目停止、或者完成 不允许操作
else
{
//---本次请求被配置规则拦截:项目状态处于进行中时,才允许操作,若此处逻辑有误,请联系开发人员修改
return Results.Json(ResponseOutput.NotOk(I18n.T("TrialResource_InterceptedProjectStatusRule")));
}
}
//添加项目 签名系统文档的时候 不做拦截 但是更新项目 签名项目文档的时候需要拦截
else if (optActions.Any(t => t == TrialOpt.AddOrUpdateTrial || t == TrialOpt.SignSystemDocNoTrialId))
{
return await next(context);
}
else
{
//如果项目相关接口没有传递trialId 会来到这里,提醒,以便修改
//---该接口参数中,没有传递项目编号,请核对。
return Results.Json(ResponseOutput.NotOk(I18n.T("TrialResource_MissingProjectNumber")));
}
}
// 如果没有 TrialGlobalLimitAttribute则直接执行后续逻辑
return await next(context);
}
}

View File

@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Authorization;
using IRaCIS.Core.Application.BusinessFilter;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Mvc;
using System;
@ -24,7 +25,7 @@ namespace IRaCIS.Core.Application.Service.MinimalApiService
{
[TrialGlobalLimit("AddOrUpdateTrial", "BeforeOngoingCantOpt", "AfterStopCannNotOpt")]
public Task<List<string>> GetProjectList1Async()
{
var list = new List<string>()
@ -37,6 +38,7 @@ namespace IRaCIS.Core.Application.Service.MinimalApiService
}
[AllowAnonymous]
[TrialGlobalLimit( "BeforeOngoingCantOpt")]
public IResponseOutput GetTest()
{