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

main
NoahLan 6 months ago
parent 54ea6747d4
commit e9fee0d664

@ -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<AbpConventionalControllerOptions> 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<AbpConventionalControllerOptions> 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;
}
}

@ -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<AbpAspNetCoreMvcOptions> 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<RemoteServiceAttribute>(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<HttpMethodActionConstraint>()
.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<HttpMethodActionConstraint>().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<AbpAspNetCoreMvcOptions> 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<RemoteServiceAttribute>(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<HttpMethodActionConstraint>()
.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<HttpMethodActionConstraint>().Any())
{
selector.ActionConstraints.Add(new HttpMethodActionConstraint(new[] { httpMethod }));
}
}
}
}

@ -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<AbpAuditingOptions>(opt =>
{
// 默认关闭,开启后有大量的审计日志
opt.IsEnabled = false;
// 审计日志过滤器
opt.AlwaysLogSelectors.Add(_ => Task.FromResult(true));
});
// 动态API
Configure<AbpAspNetCoreMvcOptions>(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<AbpDistributedCacheOptions>(opt =>
{
opt.GlobalCacheEntryOptions.SlidingExpiration = null;
// 缓存key前缀
opt.KeyPrefix = "NPin:";
});
// 关闭CSRF防伪验证
Configure<AbpAntiForgeryOptions>(opt => { opt.AutoValidate = false; });
// 设置 swagger
context.Services.AddNPinSwaggerGen<NPinWebModule>(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<AbpTenantResolveOptions>(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<AbpAuditingOptions>(opt =>
{
// 默认关闭,开启后有大量的审计日志
opt.IsEnabled = false;
// 审计日志过滤器
opt.AlwaysLogSelectors.Add(_ => Task.FromResult(true));
});
// 动态API
Configure<AbpAspNetCoreMvcOptions>(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<AbpDistributedCacheOptions>(opt =>
{
opt.GlobalCacheEntryOptions.SlidingExpiration = null;
// 缓存key前缀
opt.KeyPrefix = "NPin:";
});
// 关闭CSRF防伪验证
Configure<AbpAntiForgeryOptions>(opt => { opt.AutoValidate = false; });
// 设置 swagger
context.Services.AddNPinSwaggerGen<NPinWebModule>(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<AbpTenantResolveOptions>(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, string>(
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;
}
}
Loading…
Cancel
Save