feat: 统一API前缀配置+全局限流。

main
NoahLan 6 months ago
parent 54ea6747d4
commit e9fee0d664

@ -1,63 +1,63 @@
using System.Reflection; using System.Reflection;
using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Volo.Abp.AspNetCore.Mvc.Conventions; using Volo.Abp.AspNetCore.Mvc.Conventions;
using Volo.Abp.DependencyInjection; using Volo.Abp.DependencyInjection;
using Volo.Abp.Reflection; using Volo.Abp.Reflection;
namespace NPin.Framework.AspNetCore.Mvc; namespace NPin.Framework.AspNetCore.Mvc;
[Dependency(ServiceLifetime.Transient, ReplaceServices = true)] [Dependency(ServiceLifetime.Transient, ReplaceServices = true)]
[ExposeServices(typeof(IConventionalRouteBuilder))] [ExposeServices(typeof(IConventionalRouteBuilder))]
public class NPinConventionalRouteBuilder:ConventionalRouteBuilder public class NPinConventionalRouteBuilder:ConventionalRouteBuilder
{ {
public NPinConventionalRouteBuilder(IOptions<AbpConventionalControllerOptions> options) : base(options) public NPinConventionalRouteBuilder(IOptions<AbpConventionalControllerOptions> options) : base(options)
{ {
} }
public override string Build(string rootPath, string controllerName, ActionModel action, string httpMethod, public override string Build(string rootPath, string controllerName, ActionModel action, string httpMethod,
ConventionalControllerSetting? configuration) ConventionalControllerSetting? configuration)
{ {
var apiRoutePrefix = GetApiRoutePrefix(action, configuration); // var apiRoutePrefix = GetApiRoutePrefix(action, configuration);
var controllerNameInUrl = var controllerNameInUrl =
NormalizeUrlControllerName(rootPath, controllerName, action, httpMethod, configuration); NormalizeUrlControllerName(rootPath, controllerName, action, httpMethod, configuration);
var url = $"{apiRoutePrefix}/{rootPath}/{NormalizeControllerNameCase(controllerNameInUrl, configuration)}"; var url = $"{rootPath}/{NormalizeControllerNameCase(controllerNameInUrl, configuration)}";
// Add {id} path if needed // Add {id} path if needed
var idParameterModel = action.Parameters.FirstOrDefault(p => p.ParameterName == "id"); var idParameterModel = action.Parameters.FirstOrDefault(p => p.ParameterName == "id");
if (idParameterModel != null) if (idParameterModel != null)
{ {
if (TypeHelper.IsPrimitiveExtended(idParameterModel.ParameterType, includeEnums: true)) if (TypeHelper.IsPrimitiveExtended(idParameterModel.ParameterType, includeEnums: true))
{ {
url += "/{id}"; url += "/{id}";
} }
else else
{ {
var properties = var properties =
idParameterModel.ParameterType.GetProperties(BindingFlags.Instance | BindingFlags.Public); idParameterModel.ParameterType.GetProperties(BindingFlags.Instance | BindingFlags.Public);
foreach (var property in properties) foreach (var property in properties)
{ {
url += "/{" + NormalizeIdPropertyNameCase(property, configuration) + "}"; url += "/{" + NormalizeIdPropertyNameCase(property, configuration) + "}";
} }
} }
} }
// Add action name if needed // Add action name if needed
var actionNameInUrl = NormalizeUrlActionName(rootPath, controllerName, action, httpMethod, configuration); var actionNameInUrl = NormalizeUrlActionName(rootPath, controllerName, action, httpMethod, configuration);
if (!actionNameInUrl.IsNullOrEmpty()) if (!actionNameInUrl.IsNullOrEmpty())
{ {
url += $"/{NormalizeActionNameCase(actionNameInUrl, configuration)}"; url += $"/{NormalizeActionNameCase(actionNameInUrl, configuration)}";
// Add secondary Id // Add secondary Id
var secondaryIds = action.Parameters var secondaryIds = action.Parameters
.Where(p => p.ParameterName.EndsWith("Id", StringComparison.Ordinal)).ToList(); .Where(p => p.ParameterName.EndsWith("Id", StringComparison.Ordinal)).ToList();
if (secondaryIds.Count == 1) if (secondaryIds.Count == 1)
{ {
url += $"/{{{NormalizeSecondaryIdNameCase(secondaryIds[0], configuration)}}}"; url += $"/{{{NormalizeSecondaryIdNameCase(secondaryIds[0], configuration)}}}";
} }
} }
return url; return url;
} }
} }

@ -1,77 +1,77 @@
using Microsoft.AspNetCore.Mvc.ActionConstraints; using Microsoft.AspNetCore.Mvc.ActionConstraints;
using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Volo.Abp; using Volo.Abp;
using Volo.Abp.AspNetCore; using Volo.Abp.AspNetCore;
using Volo.Abp.AspNetCore.Mvc; using Volo.Abp.AspNetCore.Mvc;
using Volo.Abp.AspNetCore.Mvc.Conventions; using Volo.Abp.AspNetCore.Mvc.Conventions;
using Volo.Abp.DependencyInjection; using Volo.Abp.DependencyInjection;
using Volo.Abp.Reflection; using Volo.Abp.Reflection;
namespace NPin.Framework.AspNetCore.Mvc; namespace NPin.Framework.AspNetCore.Mvc;
[Dependency(ServiceLifetime.Transient, ReplaceServices = true)] [Dependency(ServiceLifetime.Transient, ReplaceServices = true)]
[ExposeServices(typeof(IAbpServiceConvention))] [ExposeServices(typeof(IAbpServiceConvention))]
public class NPinServiceConvention : AbpServiceConvention public class NPinServiceConvention : AbpServiceConvention
{ {
public NPinServiceConvention(IOptions<AbpAspNetCoreMvcOptions> options, public NPinServiceConvention(IOptions<AbpAspNetCoreMvcOptions> options,
IConventionalRouteBuilder conventionalRouteBuilder) : base(options, conventionalRouteBuilder) IConventionalRouteBuilder conventionalRouteBuilder) : base(options, conventionalRouteBuilder)
{ {
} }
protected override void ConfigureSelector(string rootPath, string controllerName, ActionModel action, protected override void ConfigureSelector(string rootPath, string controllerName, ActionModel action,
ConventionalControllerSetting? configuration) ConventionalControllerSetting? configuration)
{ {
RemoveEmptySelectors(action.Selectors); RemoveEmptySelectors(action.Selectors);
var remoteServiceAtt = var remoteServiceAtt =
ReflectionHelper.GetSingleAttributeOrDefault<RemoteServiceAttribute>(action.ActionMethod); ReflectionHelper.GetSingleAttributeOrDefault<RemoteServiceAttribute>(action.ActionMethod);
if (remoteServiceAtt != null && !remoteServiceAtt.IsEnabledFor(action.ActionMethod)) if (remoteServiceAtt != null && !remoteServiceAtt.IsEnabledFor(action.ActionMethod))
{ {
return; return;
} }
if (!action.Selectors.Any()) if (!action.Selectors.Any())
{ {
AddAbpServiceSelector(rootPath, controllerName, action, configuration); AddAbpServiceSelector(rootPath, controllerName, action, configuration);
} }
else else
{ {
NormalizeSelectorRoutes(rootPath, controllerName, action, configuration); NormalizeSelectorRoutes(rootPath, controllerName, action, configuration);
} }
} }
protected override void NormalizeSelectorRoutes(string rootPath, string controllerName, ActionModel action, protected override void NormalizeSelectorRoutes(string rootPath, string controllerName, ActionModel action,
ConventionalControllerSetting? configuration) ConventionalControllerSetting? configuration)
{ {
foreach (var selector in action.Selectors) foreach (var selector in action.Selectors)
{ {
var httpMethod = selector.ActionConstraints var httpMethod = selector.ActionConstraints
.OfType<HttpMethodActionConstraint>() .OfType<HttpMethodActionConstraint>()
.FirstOrDefault()? .FirstOrDefault()?
.HttpMethods? .HttpMethods?
.FirstOrDefault() ?? SelectHttpMethod(action, configuration); .FirstOrDefault() ?? SelectHttpMethod(action, configuration);
if (selector.AttributeRouteModel == null) if (selector.AttributeRouteModel == null)
{ {
selector.AttributeRouteModel = selector.AttributeRouteModel =
CreateAbpServiceAttributeRouteModel(rootPath, controllerName, action, httpMethod, configuration); CreateAbpServiceAttributeRouteModel(rootPath, controllerName, action, httpMethod, configuration);
} }
else else
{ {
var template = selector.AttributeRouteModel.Template; var template = selector.AttributeRouteModel.Template;
if (!template.StartsWith('/')) if (!template.StartsWith('/'))
{ {
var route = $"{AbpAspNetCoreConsts.DefaultApiPrefix}/{rootPath}/{template}"; var route = $"{rootPath}/{template}";
selector.AttributeRouteModel.Template = route; selector.AttributeRouteModel.Template = route;
} }
} }
if (!selector.ActionConstraints.OfType<HttpMethodActionConstraint>().Any()) if (!selector.ActionConstraints.OfType<HttpMethodActionConstraint>().Any())
{ {
selector.ActionConstraints.Add(new HttpMethodActionConstraint(new[] { httpMethod })); selector.ActionConstraints.Add(new HttpMethodActionConstraint(new[] { httpMethod }));
} }
} }
} }
} }

@ -1,186 +1,228 @@
using Microsoft.AspNetCore.Authentication.JwtBearer; using System.Globalization;
using Microsoft.AspNetCore.Cors; using System.Threading.RateLimiting;
using Microsoft.OpenApi.Models; using Microsoft.AspNetCore.Authentication.JwtBearer;
using Newtonsoft.Json.Converters; using Microsoft.AspNetCore.Cors;
using NPin.Application; using Microsoft.OpenApi.Models;
using NPin.Framework.AspNetCore; using Newtonsoft.Json.Converters;
using NPin.Framework.AspNetCore.Authentication.OAuth; using NPin.Application;
using NPin.Framework.AspNetCore.Authentication.OAuth.Gitee; using NPin.Framework.AspNetCore;
using NPin.Framework.AspNetCore.Authentication.OAuth.QQ; using NPin.Framework.AspNetCore.Authentication.OAuth;
using NPin.Framework.AspNetCore.Microsoft.AspNetCore.Builder; using NPin.Framework.AspNetCore.Authentication.OAuth.Gitee;
using NPin.Framework.AspNetCore.Microsoft.Extensions.DependencyInjection; using NPin.Framework.AspNetCore.Authentication.OAuth.QQ;
using NPin.Framework.TenantManagement.Application; using NPin.Framework.AspNetCore.Microsoft.AspNetCore.Builder;
using NPin.SqlSugarCore; using NPin.Framework.AspNetCore.Microsoft.Extensions.DependencyInjection;
using Volo.Abp.AspNetCore.Authentication.JwtBearer; using NPin.Framework.TenantManagement.Application;
using Volo.Abp.AspNetCore.MultiTenancy; using NPin.Framework.Upms.Application;
using Volo.Abp.AspNetCore.Mvc; using NPin.SqlSugarCore;
using Volo.Abp.AspNetCore.Mvc.AntiForgery; using Volo.Abp.AspNetCore.Authentication.JwtBearer;
using Volo.Abp.AspNetCore.Serilog; using Volo.Abp.AspNetCore.MultiTenancy;
using Volo.Abp.Auditing; using Volo.Abp.AspNetCore.Mvc;
using Volo.Abp.Autofac; using Volo.Abp.AspNetCore.Mvc.AntiForgery;
using Volo.Abp.Caching; using Volo.Abp.AspNetCore.Serilog;
using Volo.Abp.MultiTenancy; using Volo.Abp.Auditing;
using Volo.Abp.Swashbuckle; using Volo.Abp.Autofac;
using Volo.Abp.Caching;
namespace NPin; using Volo.Abp.MultiTenancy;
using Volo.Abp.Swashbuckle;
[DependsOn(
typeof(NPinSqlSugarCoreModule), namespace NPin;
typeof(NPinApplicationModule),
// Abp modules [DependsOn(
typeof(AbpAspNetCoreMultiTenancyModule), typeof(NPinSqlSugarCoreModule),
typeof(AbpAspNetCoreMvcModule), typeof(NPinApplicationModule),
typeof(AbpAutofacModule), // Abp modules
typeof(AbpSwashbuckleModule), typeof(AbpAspNetCoreMultiTenancyModule),
typeof(AbpAspNetCoreSerilogModule), typeof(AbpAspNetCoreMvcModule),
typeof(AbpAuditingModule), typeof(AbpAutofacModule),
typeof(AbpAspNetCoreAuthenticationJwtBearerModule), typeof(AbpSwashbuckleModule),
// Framework modules typeof(AbpAspNetCoreSerilogModule),
typeof(NPinFrameworkAspNetCoreModule), typeof(AbpAuditingModule),
typeof(NPinFrameworkAspNetCoreAuthenticationOAuthModule) typeof(AbpAspNetCoreAuthenticationJwtBearerModule),
)] // Framework modules
public class NPinWebModule : AbpModule typeof(NPinFrameworkAspNetCoreModule),
{ typeof(NPinFrameworkAspNetCoreAuthenticationOAuthModule)
private const string DefaultCorsPolicyName = "Default"; )]
public class NPinWebModule : AbpModule
public override Task ConfigureServicesAsync(ServiceConfigurationContext context) {
{ private const string DefaultCorsPolicyName = "Default";
var configuration = context.Services.GetConfiguration();
var services = context.Services; public override Task ConfigureServicesAsync(ServiceConfigurationContext context)
{
// 请求日志 var configuration = context.Services.GetConfiguration();
Configure<AbpAuditingOptions>(opt => var services = context.Services;
{
// 默认关闭,开启后有大量的审计日志 // 请求日志
opt.IsEnabled = false; Configure<AbpAuditingOptions>(opt =>
// 审计日志过滤器 {
opt.AlwaysLogSelectors.Add(_ => Task.FromResult(true)); // 默认关闭,开启后有大量的审计日志
}); opt.IsEnabled = false;
// 审计日志过滤器
// 动态API opt.AlwaysLogSelectors.Add(_ => Task.FromResult(true));
Configure<AbpAspNetCoreMvcOptions>(opt => });
{
opt.ConventionalControllers.Create(typeof(NPinApplicationModule).Assembly, // 动态API
opts => opts.RemoteServiceName = "default"); Configure<AbpAspNetCoreMvcOptions>(opt =>
// TODO 添加其它模块的动态API {
// TODO Rbac bbs code-gen opt.ConventionalControllers.Create(typeof(NPinApplicationModule).Assembly,
opt.ConventionalControllers.Create(typeof(NPinFrameworkTenantManagementApplicationModule).Assembly, opts => opts.RemoteServiceName = "default");
opts => opts.RemoteServiceName = "tenant-management"); // TODO 添加其它模块的动态API
}); // TODO bbs code-gen
opt.ConventionalControllers.Create(typeof(NPinFrameworkUpmsApplicationModule).Assembly,
// Api格式配置 opts => opts.RemoteServiceName = "upms");
services.AddControllers().AddNewtonsoftJson(opt => opt.ConventionalControllers.Create(typeof(NPinFrameworkTenantManagementApplicationModule).Assembly,
{ opts => opts.RemoteServiceName = "tenant-management");
// 时间格式
opt.SerializerSettings.DateFormatString = "yyyy-MM-dd HH:mm:ss"; // 统一API前缀
// 枚举 <-> string 转换 opt.ConventionalControllers.ConventionalControllerSettings.ForEach(x => x.RootPath = "api");
opt.SerializerSettings.Converters.Add(new StringEnumConverter()); });
});
// Api格式配置
// 设置分布式缓存不要过期滑动时间20分钟 services.AddControllers().AddNewtonsoftJson(opt =>
Configure<AbpDistributedCacheOptions>(opt => {
{ // 时间格式
opt.GlobalCacheEntryOptions.SlidingExpiration = null; opt.SerializerSettings.DateFormatString = "yyyy-MM-dd HH:mm:ss";
// 缓存key前缀 // 枚举 <-> string 转换
opt.KeyPrefix = "NPin:"; opt.SerializerSettings.Converters.Add(new StringEnumConverter());
}); });
// 关闭CSRF防伪验证 // 设置分布式缓存不要过期滑动时间20分钟
Configure<AbpAntiForgeryOptions>(opt => { opt.AutoValidate = false; }); Configure<AbpDistributedCacheOptions>(opt =>
{
// 设置 swagger opt.GlobalCacheEntryOptions.SlidingExpiration = null;
context.Services.AddNPinSwaggerGen<NPinWebModule>(opt => // 缓存key前缀
{ opt.KeyPrefix = "NPin:";
opt.SwaggerDoc("default", new OpenApiInfo });
{
Title = "NPin.Framework", // 关闭CSRF防伪验证
Version = "v1", Configure<AbpAntiForgeryOptions>(opt => { opt.AutoValidate = false; });
Description = "NPin.Framework API文档"
}); // 设置 swagger
}); context.Services.AddNPinSwaggerGen<NPinWebModule>(opt =>
{
// 配置跨域 opt.SwaggerDoc("default", new OpenApiInfo
context.Services.AddCors(opt => {
{ Title = "NPin.Framework",
opt.AddPolicy(DefaultCorsPolicyName, builder => Version = "v1",
{ Description = "NPin.Framework API文档"
builder.WithOrigins( });
configuration["App:CorsOrigins"]!.Split(";", StringSplitOptions.RemoveEmptyEntries) });
.Select(o => o.RemovePostFix("/"))
.ToArray() // 配置跨域
) context.Services.AddCors(opt =>
.WithAbpExposedHeaders() {
.SetIsOriginAllowedToAllowWildcardSubdomains() opt.AddPolicy(DefaultCorsPolicyName, builder =>
.AllowAnyHeader() {
.AllowAnyMethod() builder.WithOrigins(
.AllowCredentials(); configuration["App:CorsOrigins"]!.Split(";", StringSplitOptions.RemoveEmptyEntries)
}); .Select(o => o.RemovePostFix("/"))
}); .ToArray()
)
// 配置 多租户 .WithAbpExposedHeaders()
Configure<AbpTenantResolveOptions>(opt => .SetIsOriginAllowedToAllowWildcardSubdomains()
{ .AllowAnyHeader()
// 只需要 Header 方式的多租户 .AllowAnyMethod()
// Cookie 方式在 jwt上有坑 .AllowCredentials();
opt.TenantResolvers.Clear(); });
opt.TenantResolvers.Add(new HeaderTenantResolveContributor()); });
});
// 配置 多租户
// 配置 JWT 鉴权 Configure<AbpTenantResolveOptions>(opt =>
// var jwtOptions = configuration.GetSection(nameof(JwtOptions)) {
// 只需要 Header 方式的多租户
context.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) // Cookie 方式在 jwt上有坑
// .AddJwtBearer(opt => { }) opt.TenantResolvers.Clear();
// .AddJwtBearer(opt => { }) opt.TenantResolvers.Add(new HeaderTenantResolveContributor());
.AddQQ(opt => { configuration.GetSection("OAuth:QQ").Bind(opt); }) });
.AddGitee(opt => { configuration.GetSection("OAuth:Gitee").Bind(opt); });
//速率限制
// 授权 //每60秒限制100个请求滑块添加分6段
context.Services.AddAuthorization(); context.Services.AddRateLimiter(opt =>
{
return Task.CompletedTask; opt.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
} opt.OnRejected = (ctx, _) =>
{
public override Task OnApplicationInitializationAsync(ApplicationInitializationContext context) if (ctx.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter))
{ {
var app = context.GetApplicationBuilder(); ctx.HttpContext.Response.Headers.RetryAfter =
((int)retryAfter.TotalSeconds).ToString(NumberFormatInfo.InvariantInfo);
app.UseRouting(); }
// Cors ctx.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
app.UseCors(DefaultCorsPolicyName); ctx.HttpContext.Response.WriteAsync("太多请求了,请稍后再试。");
return new ValueTask();
// TODO 无感Token refresh };
// 多租户 // 全局使用,链式表达式
app.UseMultiTenancy(); opt.GlobalLimiter = PartitionedRateLimiter.CreateChained(PartitionedRateLimiter.Create<HttpContext, string>(
httpContext =>
// Swagger {
app.UseNPinSwagger(); var userAgent = httpContext.Request.Headers.UserAgent.ToString();
return RateLimitPartition.GetSlidingWindowLimiter(userAgent, _ =>
// 中间件 new SlidingWindowRateLimiterOptions
app.UseNPinApiMiddleware(); {
PermitLimit = 1000,
// 静态资源 Window = TimeSpan.FromSeconds(60),
app.UseStaticFiles("/api/app/wwwroot"); SegmentsPerWindow = 6,
app.UseDefaultFiles(); QueueProcessingOrder = QueueProcessingOrder.OldestFirst
app.UseDirectoryBrowser("/api/app/wwwroot"); });
}));
// 工作单元 });
app.UseUnitOfWork();
// 配置 JWT 鉴权
// 授权 // var jwtOptions = configuration.GetSection(nameof(JwtOptions))
app.UseAuthorization();
context.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
// 审计 // .AddJwtBearer(opt => { })
app.UseAuditing(); // .AddJwtBearer(opt => { })
.AddQQ(opt => { configuration.GetSection("OAuth:QQ").Bind(opt); })
// 日志 .AddGitee(opt => { configuration.GetSection("OAuth:Gitee").Bind(opt); });
app.UseAbpSerilogEnrichers();
// 授权
// 终端节点 context.Services.AddAuthorization();
app.UseConfiguredEndpoints();
return Task.CompletedTask;
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;
}
} }
Loading…
Cancel
Save