diff --git a/framework/NPin.Framework.AspNetCore/Mvc/NPinConventionalRouteBuilder.cs b/framework/NPin.Framework.AspNetCore/Mvc/NPinConventionalRouteBuilder.cs index ec6d225..ae39425 100644 --- a/framework/NPin.Framework.AspNetCore/Mvc/NPinConventionalRouteBuilder.cs +++ b/framework/NPin.Framework.AspNetCore/Mvc/NPinConventionalRouteBuilder.cs @@ -1,63 +1,63 @@ -using System.Reflection; -using Microsoft.AspNetCore.Mvc.ApplicationModels; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using Volo.Abp.AspNetCore.Mvc.Conventions; -using Volo.Abp.DependencyInjection; -using Volo.Abp.Reflection; - -namespace NPin.Framework.AspNetCore.Mvc; - -[Dependency(ServiceLifetime.Transient, ReplaceServices = true)] -[ExposeServices(typeof(IConventionalRouteBuilder))] -public class NPinConventionalRouteBuilder:ConventionalRouteBuilder -{ - public NPinConventionalRouteBuilder(IOptions options) : base(options) - { - } - - public override string Build(string rootPath, string controllerName, ActionModel action, string httpMethod, - ConventionalControllerSetting? configuration) - { - var apiRoutePrefix = GetApiRoutePrefix(action, configuration); - var controllerNameInUrl = - NormalizeUrlControllerName(rootPath, controllerName, action, httpMethod, configuration); - var url = $"{apiRoutePrefix}/{rootPath}/{NormalizeControllerNameCase(controllerNameInUrl, configuration)}"; - - // Add {id} path if needed - var idParameterModel = action.Parameters.FirstOrDefault(p => p.ParameterName == "id"); - if (idParameterModel != null) - { - if (TypeHelper.IsPrimitiveExtended(idParameterModel.ParameterType, includeEnums: true)) - { - url += "/{id}"; - } - else - { - var properties = - idParameterModel.ParameterType.GetProperties(BindingFlags.Instance | BindingFlags.Public); - foreach (var property in properties) - { - url += "/{" + NormalizeIdPropertyNameCase(property, configuration) + "}"; - } - } - } - - // Add action name if needed - var actionNameInUrl = NormalizeUrlActionName(rootPath, controllerName, action, httpMethod, configuration); - if (!actionNameInUrl.IsNullOrEmpty()) - { - url += $"/{NormalizeActionNameCase(actionNameInUrl, configuration)}"; - - // Add secondary Id - var secondaryIds = action.Parameters - .Where(p => p.ParameterName.EndsWith("Id", StringComparison.Ordinal)).ToList(); - if (secondaryIds.Count == 1) - { - url += $"/{{{NormalizeSecondaryIdNameCase(secondaryIds[0], configuration)}}}"; - } - } - - return url; - } +using System.Reflection; +using Microsoft.AspNetCore.Mvc.ApplicationModels; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Volo.Abp.AspNetCore.Mvc.Conventions; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Reflection; + +namespace NPin.Framework.AspNetCore.Mvc; + +[Dependency(ServiceLifetime.Transient, ReplaceServices = true)] +[ExposeServices(typeof(IConventionalRouteBuilder))] +public class NPinConventionalRouteBuilder:ConventionalRouteBuilder +{ + public NPinConventionalRouteBuilder(IOptions options) : base(options) + { + } + + public override string Build(string rootPath, string controllerName, ActionModel action, string httpMethod, + ConventionalControllerSetting? configuration) + { + // var apiRoutePrefix = GetApiRoutePrefix(action, configuration); + var controllerNameInUrl = + NormalizeUrlControllerName(rootPath, controllerName, action, httpMethod, configuration); + var url = $"{rootPath}/{NormalizeControllerNameCase(controllerNameInUrl, configuration)}"; + + // Add {id} path if needed + var idParameterModel = action.Parameters.FirstOrDefault(p => p.ParameterName == "id"); + if (idParameterModel != null) + { + if (TypeHelper.IsPrimitiveExtended(idParameterModel.ParameterType, includeEnums: true)) + { + url += "/{id}"; + } + else + { + var properties = + idParameterModel.ParameterType.GetProperties(BindingFlags.Instance | BindingFlags.Public); + foreach (var property in properties) + { + url += "/{" + NormalizeIdPropertyNameCase(property, configuration) + "}"; + } + } + } + + // Add action name if needed + var actionNameInUrl = NormalizeUrlActionName(rootPath, controllerName, action, httpMethod, configuration); + if (!actionNameInUrl.IsNullOrEmpty()) + { + url += $"/{NormalizeActionNameCase(actionNameInUrl, configuration)}"; + + // Add secondary Id + var secondaryIds = action.Parameters + .Where(p => p.ParameterName.EndsWith("Id", StringComparison.Ordinal)).ToList(); + if (secondaryIds.Count == 1) + { + url += $"/{{{NormalizeSecondaryIdNameCase(secondaryIds[0], configuration)}}}"; + } + } + + return url; + } } \ No newline at end of file diff --git a/framework/NPin.Framework.AspNetCore/Mvc/NPinServiceConvention.cs b/framework/NPin.Framework.AspNetCore/Mvc/NPinServiceConvention.cs index fa9f9c9..7094eb0 100644 --- a/framework/NPin.Framework.AspNetCore/Mvc/NPinServiceConvention.cs +++ b/framework/NPin.Framework.AspNetCore/Mvc/NPinServiceConvention.cs @@ -1,77 +1,77 @@ -using Microsoft.AspNetCore.Mvc.ActionConstraints; -using Microsoft.AspNetCore.Mvc.ApplicationModels; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using Volo.Abp; -using Volo.Abp.AspNetCore; -using Volo.Abp.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc.Conventions; -using Volo.Abp.DependencyInjection; -using Volo.Abp.Reflection; - -namespace NPin.Framework.AspNetCore.Mvc; - -[Dependency(ServiceLifetime.Transient, ReplaceServices = true)] -[ExposeServices(typeof(IAbpServiceConvention))] -public class NPinServiceConvention : AbpServiceConvention -{ - public NPinServiceConvention(IOptions options, - IConventionalRouteBuilder conventionalRouteBuilder) : base(options, conventionalRouteBuilder) - { - } - - protected override void ConfigureSelector(string rootPath, string controllerName, ActionModel action, - ConventionalControllerSetting? configuration) - { - RemoveEmptySelectors(action.Selectors); - - var remoteServiceAtt = - ReflectionHelper.GetSingleAttributeOrDefault(action.ActionMethod); - if (remoteServiceAtt != null && !remoteServiceAtt.IsEnabledFor(action.ActionMethod)) - { - return; - } - - if (!action.Selectors.Any()) - { - AddAbpServiceSelector(rootPath, controllerName, action, configuration); - } - else - { - NormalizeSelectorRoutes(rootPath, controllerName, action, configuration); - } - } - - protected override void NormalizeSelectorRoutes(string rootPath, string controllerName, ActionModel action, - ConventionalControllerSetting? configuration) - { - foreach (var selector in action.Selectors) - { - var httpMethod = selector.ActionConstraints - .OfType() - .FirstOrDefault()? - .HttpMethods? - .FirstOrDefault() ?? SelectHttpMethod(action, configuration); - - if (selector.AttributeRouteModel == null) - { - selector.AttributeRouteModel = - CreateAbpServiceAttributeRouteModel(rootPath, controllerName, action, httpMethod, configuration); - } - else - { - var template = selector.AttributeRouteModel.Template; - if (!template.StartsWith('/')) - { - var route = $"{AbpAspNetCoreConsts.DefaultApiPrefix}/{rootPath}/{template}"; - selector.AttributeRouteModel.Template = route; - } - } - - if (!selector.ActionConstraints.OfType().Any()) - { - selector.ActionConstraints.Add(new HttpMethodActionConstraint(new[] { httpMethod })); - } - } - } +using Microsoft.AspNetCore.Mvc.ActionConstraints; +using Microsoft.AspNetCore.Mvc.ApplicationModels; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Volo.Abp; +using Volo.Abp.AspNetCore; +using Volo.Abp.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc.Conventions; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Reflection; + +namespace NPin.Framework.AspNetCore.Mvc; + +[Dependency(ServiceLifetime.Transient, ReplaceServices = true)] +[ExposeServices(typeof(IAbpServiceConvention))] +public class NPinServiceConvention : AbpServiceConvention +{ + public NPinServiceConvention(IOptions options, + IConventionalRouteBuilder conventionalRouteBuilder) : base(options, conventionalRouteBuilder) + { + } + + protected override void ConfigureSelector(string rootPath, string controllerName, ActionModel action, + ConventionalControllerSetting? configuration) + { + RemoveEmptySelectors(action.Selectors); + + var remoteServiceAtt = + ReflectionHelper.GetSingleAttributeOrDefault(action.ActionMethod); + if (remoteServiceAtt != null && !remoteServiceAtt.IsEnabledFor(action.ActionMethod)) + { + return; + } + + if (!action.Selectors.Any()) + { + AddAbpServiceSelector(rootPath, controllerName, action, configuration); + } + else + { + NormalizeSelectorRoutes(rootPath, controllerName, action, configuration); + } + } + + protected override void NormalizeSelectorRoutes(string rootPath, string controllerName, ActionModel action, + ConventionalControllerSetting? configuration) + { + foreach (var selector in action.Selectors) + { + var httpMethod = selector.ActionConstraints + .OfType() + .FirstOrDefault()? + .HttpMethods? + .FirstOrDefault() ?? SelectHttpMethod(action, configuration); + + if (selector.AttributeRouteModel == null) + { + selector.AttributeRouteModel = + CreateAbpServiceAttributeRouteModel(rootPath, controllerName, action, httpMethod, configuration); + } + else + { + var template = selector.AttributeRouteModel.Template; + if (!template.StartsWith('/')) + { + var route = $"{rootPath}/{template}"; + selector.AttributeRouteModel.Template = route; + } + } + + if (!selector.ActionConstraints.OfType().Any()) + { + selector.ActionConstraints.Add(new HttpMethodActionConstraint(new[] { httpMethod })); + } + } + } } \ No newline at end of file diff --git a/src/NPin.Web/NPinWebModule.cs b/src/NPin.Web/NPinWebModule.cs index bf35837..7cd4d81 100644 --- a/src/NPin.Web/NPinWebModule.cs +++ b/src/NPin.Web/NPinWebModule.cs @@ -1,186 +1,228 @@ -using Microsoft.AspNetCore.Authentication.JwtBearer; -using Microsoft.AspNetCore.Cors; -using Microsoft.OpenApi.Models; -using Newtonsoft.Json.Converters; -using NPin.Application; -using NPin.Framework.AspNetCore; -using NPin.Framework.AspNetCore.Authentication.OAuth; -using NPin.Framework.AspNetCore.Authentication.OAuth.Gitee; -using NPin.Framework.AspNetCore.Authentication.OAuth.QQ; -using NPin.Framework.AspNetCore.Microsoft.AspNetCore.Builder; -using NPin.Framework.AspNetCore.Microsoft.Extensions.DependencyInjection; -using NPin.Framework.TenantManagement.Application; -using NPin.SqlSugarCore; -using Volo.Abp.AspNetCore.Authentication.JwtBearer; -using Volo.Abp.AspNetCore.MultiTenancy; -using Volo.Abp.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc.AntiForgery; -using Volo.Abp.AspNetCore.Serilog; -using Volo.Abp.Auditing; -using Volo.Abp.Autofac; -using Volo.Abp.Caching; -using Volo.Abp.MultiTenancy; -using Volo.Abp.Swashbuckle; - -namespace NPin; - -[DependsOn( - typeof(NPinSqlSugarCoreModule), - typeof(NPinApplicationModule), - // Abp modules - typeof(AbpAspNetCoreMultiTenancyModule), - typeof(AbpAspNetCoreMvcModule), - typeof(AbpAutofacModule), - typeof(AbpSwashbuckleModule), - typeof(AbpAspNetCoreSerilogModule), - typeof(AbpAuditingModule), - typeof(AbpAspNetCoreAuthenticationJwtBearerModule), - // Framework modules - typeof(NPinFrameworkAspNetCoreModule), - typeof(NPinFrameworkAspNetCoreAuthenticationOAuthModule) -)] -public class NPinWebModule : AbpModule -{ - private const string DefaultCorsPolicyName = "Default"; - - public override Task ConfigureServicesAsync(ServiceConfigurationContext context) - { - var configuration = context.Services.GetConfiguration(); - var services = context.Services; - - // 请求日志 - Configure(opt => - { - // 默认关闭,开启后有大量的审计日志 - opt.IsEnabled = false; - // 审计日志过滤器 - opt.AlwaysLogSelectors.Add(_ => Task.FromResult(true)); - }); - - // 动态API - Configure(opt => - { - opt.ConventionalControllers.Create(typeof(NPinApplicationModule).Assembly, - opts => opts.RemoteServiceName = "default"); - // TODO 添加其它模块的动态API - // TODO Rbac bbs code-gen - opt.ConventionalControllers.Create(typeof(NPinFrameworkTenantManagementApplicationModule).Assembly, - opts => opts.RemoteServiceName = "tenant-management"); - }); - - // Api格式配置 - services.AddControllers().AddNewtonsoftJson(opt => - { - // 时间格式 - opt.SerializerSettings.DateFormatString = "yyyy-MM-dd HH:mm:ss"; - // 枚举 <-> string 转换 - opt.SerializerSettings.Converters.Add(new StringEnumConverter()); - }); - - // 设置分布式缓存不要过期,滑动时间20分钟 - Configure(opt => - { - opt.GlobalCacheEntryOptions.SlidingExpiration = null; - // 缓存key前缀 - opt.KeyPrefix = "NPin:"; - }); - - // 关闭CSRF防伪验证 - Configure(opt => { opt.AutoValidate = false; }); - - // 设置 swagger - context.Services.AddNPinSwaggerGen(opt => - { - opt.SwaggerDoc("default", new OpenApiInfo - { - Title = "NPin.Framework", - Version = "v1", - Description = "NPin.Framework API文档" - }); - }); - - // 配置跨域 - context.Services.AddCors(opt => - { - opt.AddPolicy(DefaultCorsPolicyName, builder => - { - builder.WithOrigins( - configuration["App:CorsOrigins"]!.Split(";", StringSplitOptions.RemoveEmptyEntries) - .Select(o => o.RemovePostFix("/")) - .ToArray() - ) - .WithAbpExposedHeaders() - .SetIsOriginAllowedToAllowWildcardSubdomains() - .AllowAnyHeader() - .AllowAnyMethod() - .AllowCredentials(); - }); - }); - - // 配置 多租户 - Configure(opt => - { - // 只需要 Header 方式的多租户 - // Cookie 方式在 jwt上有坑 - opt.TenantResolvers.Clear(); - opt.TenantResolvers.Add(new HeaderTenantResolveContributor()); - }); - - // 配置 JWT 鉴权 - // var jwtOptions = configuration.GetSection(nameof(JwtOptions)) - - context.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) - // .AddJwtBearer(opt => { }) - // .AddJwtBearer(opt => { }) - .AddQQ(opt => { configuration.GetSection("OAuth:QQ").Bind(opt); }) - .AddGitee(opt => { configuration.GetSection("OAuth:Gitee").Bind(opt); }); - - // 授权 - context.Services.AddAuthorization(); - - return Task.CompletedTask; - } - - public override Task OnApplicationInitializationAsync(ApplicationInitializationContext context) - { - var app = context.GetApplicationBuilder(); - - app.UseRouting(); - - // Cors - app.UseCors(DefaultCorsPolicyName); - - // TODO 无感Token refresh - - // 多租户 - app.UseMultiTenancy(); - - // Swagger - app.UseNPinSwagger(); - - // 中间件 - app.UseNPinApiMiddleware(); - - // 静态资源 - app.UseStaticFiles("/api/app/wwwroot"); - app.UseDefaultFiles(); - app.UseDirectoryBrowser("/api/app/wwwroot"); - - // 工作单元 - app.UseUnitOfWork(); - - // 授权 - app.UseAuthorization(); - - // 审计 - app.UseAuditing(); - - // 日志 - app.UseAbpSerilogEnrichers(); - - // 终端节点 - app.UseConfiguredEndpoints(); - - return Task.CompletedTask; - } +using System.Globalization; +using System.Threading.RateLimiting; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Cors; +using Microsoft.OpenApi.Models; +using Newtonsoft.Json.Converters; +using NPin.Application; +using NPin.Framework.AspNetCore; +using NPin.Framework.AspNetCore.Authentication.OAuth; +using NPin.Framework.AspNetCore.Authentication.OAuth.Gitee; +using NPin.Framework.AspNetCore.Authentication.OAuth.QQ; +using NPin.Framework.AspNetCore.Microsoft.AspNetCore.Builder; +using NPin.Framework.AspNetCore.Microsoft.Extensions.DependencyInjection; +using NPin.Framework.TenantManagement.Application; +using NPin.Framework.Upms.Application; +using NPin.SqlSugarCore; +using Volo.Abp.AspNetCore.Authentication.JwtBearer; +using Volo.Abp.AspNetCore.MultiTenancy; +using Volo.Abp.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc.AntiForgery; +using Volo.Abp.AspNetCore.Serilog; +using Volo.Abp.Auditing; +using Volo.Abp.Autofac; +using Volo.Abp.Caching; +using Volo.Abp.MultiTenancy; +using Volo.Abp.Swashbuckle; + +namespace NPin; + +[DependsOn( + typeof(NPinSqlSugarCoreModule), + typeof(NPinApplicationModule), + // Abp modules + typeof(AbpAspNetCoreMultiTenancyModule), + typeof(AbpAspNetCoreMvcModule), + typeof(AbpAutofacModule), + typeof(AbpSwashbuckleModule), + typeof(AbpAspNetCoreSerilogModule), + typeof(AbpAuditingModule), + typeof(AbpAspNetCoreAuthenticationJwtBearerModule), + // Framework modules + typeof(NPinFrameworkAspNetCoreModule), + typeof(NPinFrameworkAspNetCoreAuthenticationOAuthModule) +)] +public class NPinWebModule : AbpModule +{ + private const string DefaultCorsPolicyName = "Default"; + + public override Task ConfigureServicesAsync(ServiceConfigurationContext context) + { + var configuration = context.Services.GetConfiguration(); + var services = context.Services; + + // 请求日志 + Configure(opt => + { + // 默认关闭,开启后有大量的审计日志 + opt.IsEnabled = false; + // 审计日志过滤器 + opt.AlwaysLogSelectors.Add(_ => Task.FromResult(true)); + }); + + // 动态API + Configure(opt => + { + opt.ConventionalControllers.Create(typeof(NPinApplicationModule).Assembly, + opts => opts.RemoteServiceName = "default"); + // TODO 添加其它模块的动态API + // TODO bbs code-gen + opt.ConventionalControllers.Create(typeof(NPinFrameworkUpmsApplicationModule).Assembly, + opts => opts.RemoteServiceName = "upms"); + opt.ConventionalControllers.Create(typeof(NPinFrameworkTenantManagementApplicationModule).Assembly, + opts => opts.RemoteServiceName = "tenant-management"); + + // 统一API前缀 + opt.ConventionalControllers.ConventionalControllerSettings.ForEach(x => x.RootPath = "api"); + }); + + // Api格式配置 + services.AddControllers().AddNewtonsoftJson(opt => + { + // 时间格式 + opt.SerializerSettings.DateFormatString = "yyyy-MM-dd HH:mm:ss"; + // 枚举 <-> string 转换 + opt.SerializerSettings.Converters.Add(new StringEnumConverter()); + }); + + // 设置分布式缓存不要过期,滑动时间20分钟 + Configure(opt => + { + opt.GlobalCacheEntryOptions.SlidingExpiration = null; + // 缓存key前缀 + opt.KeyPrefix = "NPin:"; + }); + + // 关闭CSRF防伪验证 + Configure(opt => { opt.AutoValidate = false; }); + + // 设置 swagger + context.Services.AddNPinSwaggerGen(opt => + { + opt.SwaggerDoc("default", new OpenApiInfo + { + Title = "NPin.Framework", + Version = "v1", + Description = "NPin.Framework API文档" + }); + }); + + // 配置跨域 + context.Services.AddCors(opt => + { + opt.AddPolicy(DefaultCorsPolicyName, builder => + { + builder.WithOrigins( + configuration["App:CorsOrigins"]!.Split(";", StringSplitOptions.RemoveEmptyEntries) + .Select(o => o.RemovePostFix("/")) + .ToArray() + ) + .WithAbpExposedHeaders() + .SetIsOriginAllowedToAllowWildcardSubdomains() + .AllowAnyHeader() + .AllowAnyMethod() + .AllowCredentials(); + }); + }); + + // 配置 多租户 + Configure(opt => + { + // 只需要 Header 方式的多租户 + // Cookie 方式在 jwt上有坑 + opt.TenantResolvers.Clear(); + opt.TenantResolvers.Add(new HeaderTenantResolveContributor()); + }); + + //速率限制 + //每60秒限制100个请求,滑块添加,分6段 + context.Services.AddRateLimiter(opt => + { + opt.RejectionStatusCode = StatusCodes.Status429TooManyRequests; + opt.OnRejected = (ctx, _) => + { + if (ctx.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter)) + { + ctx.HttpContext.Response.Headers.RetryAfter = + ((int)retryAfter.TotalSeconds).ToString(NumberFormatInfo.InvariantInfo); + } + + ctx.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests; + ctx.HttpContext.Response.WriteAsync("太多请求了,请稍后再试。"); + return new ValueTask(); + }; + + // 全局使用,链式表达式 + opt.GlobalLimiter = PartitionedRateLimiter.CreateChained(PartitionedRateLimiter.Create( + httpContext => + { + var userAgent = httpContext.Request.Headers.UserAgent.ToString(); + return RateLimitPartition.GetSlidingWindowLimiter(userAgent, _ => + new SlidingWindowRateLimiterOptions + { + PermitLimit = 1000, + Window = TimeSpan.FromSeconds(60), + SegmentsPerWindow = 6, + QueueProcessingOrder = QueueProcessingOrder.OldestFirst + }); + })); + }); + + // 配置 JWT 鉴权 + // var jwtOptions = configuration.GetSection(nameof(JwtOptions)) + + context.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + // .AddJwtBearer(opt => { }) + // .AddJwtBearer(opt => { }) + .AddQQ(opt => { configuration.GetSection("OAuth:QQ").Bind(opt); }) + .AddGitee(opt => { configuration.GetSection("OAuth:Gitee").Bind(opt); }); + + // 授权 + context.Services.AddAuthorization(); + + return Task.CompletedTask; + } + + public override Task OnApplicationInitializationAsync(ApplicationInitializationContext context) + { + var app = context.GetApplicationBuilder(); + + app.UseRouting(); + + // Cors + app.UseCors(DefaultCorsPolicyName); + + // TODO 无感Token refresh + + // 多租户 + app.UseMultiTenancy(); + + // Swagger + app.UseNPinSwagger(); + + // 中间件 + app.UseNPinApiMiddleware(); + + // 静态资源 + app.UseStaticFiles("/api/app/wwwroot"); + app.UseDefaultFiles(); + app.UseDirectoryBrowser("/api/app/wwwroot"); + + // 工作单元 + app.UseUnitOfWork(); + + // 授权 + app.UseAuthorization(); + + // 审计 + app.UseAuditing(); + + // 日志 + app.UseAbpSerilogEnrichers(); + + // 终端节点 + app.UseConfiguredEndpoints(); + + return Task.CompletedTask; + } } \ No newline at end of file