diff --git a/IRaCIS.Core.API/Progranm.cs b/IRaCIS.Core.API/Progranm.cs index 3384f7c9f..6c0d56193 100644 --- a/IRaCIS.Core.API/Progranm.cs +++ b/IRaCIS.Core.API/Progranm.cs @@ -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(); diff --git a/IRaCIS.Core.API/_ServiceExtensions/DynamicWebApiSetup.cs b/IRaCIS.Core.API/_ServiceExtensions/DynamicWebApiSetup.cs index d08f37322..d05731de5 100644 --- a/IRaCIS.Core.API/_ServiceExtensions/DynamicWebApiSetup.cs +++ b/IRaCIS.Core.API/_ServiceExtensions/DynamicWebApiSetup.cs @@ -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() + .AddEndpointFilter() //.AddEndpointFilter() .AddEndpointFilter() .WithGroupName("Institution").DisableAntiforgery(); diff --git a/IRaCIS.Core.Application/BusinessFilter/LegacyController/TrialGlobalLimitActionFilter.cs b/IRaCIS.Core.Application/BusinessFilter/LegacyController/TrialGlobalLimitActionFilter.cs new file mode 100644 index 000000000..4e0c9c955 --- /dev/null +++ b/IRaCIS.Core.Application/BusinessFilter/LegacyController/TrialGlobalLimitActionFilter.cs @@ -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 _trialRepository) : IAsyncActionFilter +{ + public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + // 检查当前 endpoint 是否包含 TrialGlobalLimitAttribute 特性 + var trialLimitAttribute = context.HttpContext.GetEndpoint()?.Metadata.GetMetadata(); + + 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(); + } +} diff --git a/IRaCIS.Core.Application/BusinessFilter/MinimalAPI/Todo/TrialGlobalLimitEndpointFilter.cs b/IRaCIS.Core.Application/BusinessFilter/MinimalAPI/Todo/TrialGlobalLimitEndpointFilter.cs deleted file mode 100644 index d50a6c102..000000000 --- a/IRaCIS.Core.Application/BusinessFilter/MinimalAPI/Todo/TrialGlobalLimitEndpointFilter.cs +++ /dev/null @@ -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 InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next) - { - throw new NotImplementedException(); - } -} diff --git a/IRaCIS.Core.Application/BusinessFilter/MinimalAPI/TrialGlobalLimitEndpointFilter.cs b/IRaCIS.Core.Application/BusinessFilter/MinimalAPI/TrialGlobalLimitEndpointFilter.cs new file mode 100644 index 000000000..9e02eacdc --- /dev/null +++ b/IRaCIS.Core.Application/BusinessFilter/MinimalAPI/TrialGlobalLimitEndpointFilter.cs @@ -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 _trialRepository) : IEndpointFilter + +{ + public async ValueTask InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next) + { + // 检查当前 endpoint 是否包含 TrialGlobalLimitAttribute 特性 + var trialLimitAttribute = context.HttpContext.GetEndpoint()?.Metadata.GetMetadata(); + + 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); + } +} diff --git a/IRaCIS.Core.Application/Service/MinimalApiService/TestMinimalApiService.cs b/IRaCIS.Core.Application/Service/MinimalApiService/TestMinimalApiService.cs index 381a0d56b..9f264ce66 100644 --- a/IRaCIS.Core.Application/Service/MinimalApiService/TestMinimalApiService.cs +++ b/IRaCIS.Core.Application/Service/MinimalApiService/TestMinimalApiService.cs @@ -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> GetProjectList1Async() { var list = new List() @@ -37,6 +38,7 @@ namespace IRaCIS.Core.Application.Service.MinimalApiService } [AllowAnonymous] + [TrialGlobalLimit( "BeforeOngoingCantOpt")] public IResponseOutput GetTest() {