非羁押人员管理平台
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

597 lines
27 KiB

3 months ago
using AspNetCoreRateLimit;
using ATS.NonCustodial.AdminUi.Attributes;
using ATS.NonCustodial.AdminUi.Auth;
using ATS.NonCustodial.AdminUi.Extensions;
using ATS.NonCustodial.AdminUi.Extensions.Host;
using ATS.NonCustodial.AdminUi.Extensions.ImSignalR;
using ATS.NonCustodial.AdminUi.Filters;
using ATS.NonCustodial.AdminUi.Helpers;
using ATS.NonCustodial.AdminUi.Helpers.Logs;
using ATS.NonCustodial.Application.Contracts.Interfaces.Business.IM.Notifies;
using ATS.NonCustodial.Application.Impl.Business.IM;
using ATS.NonCustodial.DynamicApi;
using ATS.NonCustodial.Shared.Common.Auth;
using ATS.NonCustodial.Shared.Common.Constants;
using ATS.NonCustodial.Shared.Common.Enums;
using ATS.NonCustodial.Shared.Tools.Cache;
using IdentityServer4.AccessTokenValidation;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Connections;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.DependencyModel;
using Microsoft.Extensions.Hosting;
using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Models;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using Serilog;
using System.IdentityModel.Tokens.Jwt;
using System.Reflection;
using System.Text;
using ATS.NonCustodial.Shared.Helpers.Http.HttpPolly;
using Yitter.IdGenerator;
using IConfiguration = Microsoft.Extensions.Configuration.IConfiguration;
namespace ATS.NonCustodial.AdminUi.Configurations
{
/// <summary>
/// Startup==> ServiceCollection 注入扩展
/// </summary>
/// Author:mxg
/// CreatedTimed:2022-05-30 08:57 PM
public static class AdminServerCollectionExtension
{
/// <summary>
/// This method gets called by the runtime. Use this method to add services to the container.
/// </summary>
/// <param name="services"></param>
/// <param name="adminUiOptions"></param>
/// <param name="configuration"></param>
/// <param name="assemblies"></param>
public static void AddAdminServices(this IServiceCollection services, AdminUiOptions adminUiOptions, IConfiguration configuration, Assembly[] assemblies)
{
//雪花漂移算法
YitIdHelper.SetIdGenerator(new IdGeneratorOptions(1) { WorkerIdBitLength = 6 });
//权限处理
services.AddScoped<IPermissionHandler, PermissionHandler>();
services.AddScoped<IHttpPollyHelper, HttpPollyHelper>();
// ClaimType不被更改
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
//用户信息
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
if (adminUiOptions.IdentityServerConfiguration.Enable) services.TryAddSingleton<IUser, UserIdentityServer>(); //is4
else services.TryAddSingleton<IUser, User>(); //jwt
//AutoMapper映射配置
services.AddAutoMapper((serviceProvider, cfg) =>
{
// 你可以在这里进行全局性的配置
cfg.AllowNullCollections = true; // 示例:允许空集合映射
// 如果你需要从服务提供器(IServiceProvider)解析服务,以便在映射配置中使用,可以使用 serviceProvider 参数
// var someService = serviceProvider.GetRequiredService<ISomeService>();
// cfg.ConstructServicesUsing(type => serviceProvider.GetService(type)); // 另一种设置服务构造函数的方法
}, assemblies);
//注册job
services.InitialQuartzJob(adminUiOptions);
//Cors 跨域
services.AddAdminApiCors(adminUiOptions);
//个推
services.AddSingleton(adminUiOptions.TweetsConfigConfiguration);
#region 身份认证授权
//signalR:https://docs.microsoft.com/zh-cn/aspnet/core/signalr/authn-and-authz?view=aspnetcore-6.0
var jwtConfig = adminUiOptions.JwtConfiguration;
services.TryAddSingleton(jwtConfig);
if (adminUiOptions.IdentityServerConfiguration.Enable)
{
//is4
services.AddAuthentication(options =>
{
options.DefaultScheme = IdentityServerAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = nameof(ResponseAuthenticationHandler); //401
options.DefaultForbidScheme = nameof(ResponseAuthenticationHandler); //403
})
.AddJwtBearer(options =>
{
options.Authority = adminUiOptions.IdentityServerConfiguration.Url;
options.RequireHttpsMetadata = false;
options.Audience = "ATS.NonCustodial.Admin.Api";
//重点在于这里;判断是SignalR的路径
options.Events = new JwtBearerEvents
{
OnMessageReceived = (context) =>
{
if (!context.HttpContext.Request.Path.HasValue) return Task.CompletedTask;
//重点在于这里;判断是SignalR的路径
var accessToken = context.HttpContext.Request.Query["access_token"];
var path = context.HttpContext.Request.Path;
if (string.IsNullOrWhiteSpace(accessToken) || !path.StartsWithSegments("/nonCustodialHub")) return Task.CompletedTask;
context.Token = accessToken;
return Task.CompletedTask;
}
};
})
.AddScheme<AuthenticationSchemeOptions, ResponseAuthenticationHandler>(nameof(ResponseAuthenticationHandler), o => { });
}
else
{
//jwt
services.AddAuthentication(options =>
{
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = nameof(ResponseAuthenticationHandler); //401
options.DefaultForbidScheme = nameof(ResponseAuthenticationHandler); //403
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = jwtConfig.Issuer,
ValidAudience = jwtConfig.Audience,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtConfig.SymmetricSecurityKey)),
ClockSkew = TimeSpan.Zero
};
//重点在于这里;判断是SignalR的路径(https://www.cnblogs.com/fger/p/11811190.html)
options.Events = new JwtBearerEvents
{
OnMessageReceived = (context) =>
{
if (!context.HttpContext.Request.Path.HasValue) return Task.CompletedTask;
//重点在于这里;判断是SignalR的路径
var accessToken = context.HttpContext.Request.Query["access_token"];
var path = context.HttpContext.Request.Path;
if (string.IsNullOrWhiteSpace(accessToken) || !path.StartsWithSegments("/nonCustodialHub")) return Task.CompletedTask;
context.Token = accessToken;
return Task.CompletedTask;
},
//此处为权限验证失败后触发的事件
OnChallenge = context =>
{
//此处代码为终止.Net Core默认的返回类型和数据结果,这个很重要哦,必须
context.HandleResponse();
//自定义自己想要返回的数据结果,我这里要返回的是Json对象,通过引用Newtonsoft.Json库进行转换
var payload = new { StatusCode = 0, Message = "身份认证失败!" };
//自定义返回的数据类型
context.Response.ContentType = "application/json";
//自定义返回状态码,默认为401 我这里改成 200
context.Response.StatusCode = StatusCodes.Status200OK;
//context.Response.StatusCode = StatusCodes.Status401Unauthorized;
//输出Json数据结果
context.Response.WriteAsync(Convert.ToString(payload));
return Task.FromResult(0);
}
};
})
.AddScheme<AuthenticationSchemeOptions, ResponseAuthenticationHandler>(nameof(ResponseAuthenticationHandler), o => { });
}
#endregion 身份认证授权
#region Swagger Api文档
if (adminUiOptions.SwaggerConfiguration.Enable)
{
services.AddSwaggerGen(options =>
{
typeof(ApiVersionEnum).GetEnumNames().ToList().ForEach(version =>
{
options.SwaggerDoc(version, new OpenApiInfo
{
Version = version,
Title = "ATS.NonCustodial.Admin.Api"
});
//c.OrderActionsBy(o => o.RelativePath);
});
options.SchemaFilter<EnumSchemaFilter>();
options.CustomOperationIds(apiDesc =>
{
var controllerAction = apiDesc.ActionDescriptor as ControllerActionDescriptor;
return controllerAction?.ControllerName + "-" + controllerAction?.ActionName;
});
options.ResolveConflictingActions(apiDescription => apiDescription.First());
options.CustomSchemaIds(x => x.FullName);
options.DocInclusionPredicate((docName, description) => true);
var xmlFiles = Directory.GetFiles(Path.Combine(AppContext.BaseDirectory, "xml"), "*.xml");
if (xmlFiles.Length > 0)
{
foreach (var xmlFile in xmlFiles)
{
options.IncludeXmlComments(xmlFile, true);
}
}
var server = new OpenApiServer()
{
Url = adminUiOptions.SwaggerConfiguration.Url,
Description = ""
};
server.Extensions.Add("extensions", new OpenApiObject
{
["copyright"] = new OpenApiString(adminUiOptions.SwaggerConfiguration.Footer)
});
//options.AddServer(server);
#region 添加设置Token的按钮
if (adminUiOptions.IdentityServerConfiguration.Enable)
{
//添加Jwt验证设置
options.AddSecurityRequirement(new OpenApiSecurityRequirement()
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Id = "oauth2",
Type = ReferenceType.SecurityScheme
}
},
new List<string>()
}
});
//统一认证
options.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme
{
Type = SecuritySchemeType.OAuth2,
Description = "oauth2登录授权",
Flows = new OpenApiOAuthFlows
{
Implicit = new OpenApiOAuthFlow
{
AuthorizationUrl = new Uri($"{adminUiOptions.IdentityServerConfiguration.Url}/connect/authorize"),
Scopes = new Dictionary<string, string>
{
{ "ATS.NonCustodial.Admin.Api", "ATS.NonCustodial.Admin.Api" }
}
},
//AuthorizationCode = new OpenApiOAuthFlow
//{
// AuthorizationUrl = new Uri($"{adminApiConfiguration.IdentityServerBaseUrl}/connect/authorize"),
// TokenUrl = new Uri($"{adminApiConfiguration.IdentityServerBaseUrl}/connect/token"),
// Scopes = new Dictionary<string, string> {
// {
// adminApiConfiguration.OidcApiName, adminApiConfiguration.ApiName }
// }
//}
}
});
options.OperationFilter<AuthorizeCheckOperationFilter>();
}
else
{
//添加Jwt验证设置
options.AddSecurityRequirement(new OpenApiSecurityRequirement()
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Id = "Bearer",
Type = ReferenceType.SecurityScheme
}
},
new List<string>()
}
});
options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
Description = "Value: Bearer {token}",
Name = "Authorization",
In = ParameterLocation.Header,
Type = SecuritySchemeType.ApiKey
});
}
#endregion 添加设置Token的按钮
//注册Api过滤器
options.DocumentFilter<ApiToSwaggerFilter>();
});
}
#endregion Swagger Api文档
//操作日志
if (adminUiOptions.LogConfiguration.EnableOperationLog) services.AddScoped<ILogHandler, LogHandler>();
#region 控制器
services.AddControllers(options =>
{
options.Filters.Add<ControllerExceptionFilter>();
options.Filters.Add<ValidateInputFilter>();
options.Filters.Add<ValidatePermissionAttribute>();
if (adminUiOptions.LogConfiguration.EnableOperationLog)
{
options.Filters.Add<ControllerLogFilter>();
}
//禁止去除ActionAsync后缀
//options.SuppressAsyncSuffixInActionNames = false;
})
//.AddFluentValidation(config =>
//{
// var assembly = Assembly.LoadFrom(Path.Combine(basePath, "ATS.NonCustodial.Admin.dll"));
// config.RegisterValidatorsFromAssembly(assembly);
//})
.AddNewtonsoftJson(options =>
{
//忽略循环引用
options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
//使用驼峰 首字母小写
options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
//设置时间格式
options.SerializerSettings.DateFormatString = "yyyy-MM-dd HH:mm:ss";
})
.AddControllersAsServices();
#endregion 控制器
services.AddHttpClient();
#region 缓存
var cacheConfig = adminUiOptions.CacheConfiguration;
if (cacheConfig.Type == CacheTypeEnum.Redis)
{
var csredis = new CSRedis.CSRedisClient(cacheConfig.Redis.ConnectionString);
RedisHelper.Initialization(csredis);
services.AddSingleton<ICacheTool, RedisCacheTool>();
}
else
{
services.AddMemoryCache();
services.AddSingleton<ICacheTool, MemoryCacheTool>();
}
#endregion 缓存
#region singalR
// Change to use Name as the user identifier for SignalR
// WARNING: This requires that the source of your JWT token
// ensures that the Name claim is unique!
// If the Name claim isn't unique, users could receive messages
// intended for a different user!
services.AddSingleton<IUserIdProvider, NameUserIdProvider>();
//SignalR https://docs.microsoft.com/zh-cn/aspnet/core/signalr/configuration?view=aspnetcore-6.0&tabs=dotnet
services
.AddSignalR()
.AddHubOptions<NonCustodialHub>(options =>
{
const int keepAliveIntervalInSeconds = 60;
options.EnableDetailedErrors = true;
//客户端发保持连接请求到服务端最长间隔,默认30秒
options.ClientTimeoutInterval = TimeSpan.FromSeconds(2 * keepAliveIntervalInSeconds);
options.HandshakeTimeout = TimeSpan.FromSeconds(keepAliveIntervalInSeconds);
//服务端发保持连接请求到客户端间隔,默认15秒
options.KeepAliveInterval = TimeSpan.FromSeconds(keepAliveIntervalInSeconds);
})
.AddJsonProtocol(options =>
{
options.PayloadSerializerOptions.PropertyNamingPolicy = null;
});
services.AddSingleton(typeof(IClientNotifyService), typeof(ClientNotifyService));
#endregion singalR
//性能分析 (https://miniprofiler.com/)
//默认的index.html页面可以从如下链接下载
//https://github.com/domaindrivendev/Swashbuckle.AspNetCore/blob/master/src/Swashbuckle.AspNetCore.SwaggerUI/index.html
if (adminUiOptions.SwaggerConfiguration.EnableMiniProfiler) services.AddMiniProfiler();//AddComtorizedMiniProfiler();
//动态api
services.AddDynamicApi(options =>
{
var assemblies = DependencyContext.Default.RuntimeLibraries
.Where(a => a.Name.EndsWith("Service"))
.Select(o => Assembly.Load(new AssemblyName(o.Name))).ToArray();
options.AddAssemblyOptions(assemblies);
});
}
/// <summary>
/// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
/// </summary>
/// <param name="app"></param>
/// <param name="env"></param>
/// <param name="adminUiOptions"></param>
/// <param name="indexStream"></param>
public static void ConfigApp(IApplicationBuilder app, IWebHostEnvironment env, AdminUiOptions adminUiOptions, Stream? indexStream)
{
//IP限流
if (adminUiOptions.RateLimitConfiguration.Enable) app.UseIpRateLimiting();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
//静态文件
app.UseDefaultFiles();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseUploadConfig();
//激活中间件,启用MiniProfiler服务,放在UseEndpoints方法之前
if (adminUiOptions.SwaggerConfiguration.EnableMiniProfiler) app.UseMiniProfiler();
//路由
app.UseRouting();
//跨域
app.UseCors(AdminConstant.requestPolicyName);
//认证
app.UseAuthentication();
//授权
app.UseAuthorization();
//配置端点(include signalR)
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
//即时通讯
endpoints.MapHub<NonCustodialHub>("/nonCustodialHub", options =>
{
options.Transports =
HttpTransportType.WebSockets |
HttpTransportType.LongPolling;
options.LongPolling.PollTimeout = TimeSpan.FromSeconds(30);
}).RequireCors(t =>
t.WithOrigins(adminUiOptions.UrlsConfiguration.CorUrls.Any()
? adminUiOptions.UrlsConfiguration.CorUrls.ToArray()
: new string[] { })
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials()
);
});
//解决Ubuntu下Nginx代理不能获取IP问题
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
});
#region Swagger Api文档
if (!env.IsDevelopment() && !adminUiOptions.SwaggerConfiguration.Enable) return;
app.UseSwagger();
app.UseSwaggerUI(c =>
{
typeof(ApiVersionEnum).GetEnumNames().OrderByDescending(e => e).ToList().ForEach(version =>
{
c.SwaggerEndpoint($"/swagger/{version}/swagger.json", $"ATS.NonCustodial.Admin {version}");
});
c.RoutePrefix = "";//直接根目录访问,如果是IIS发布可以注释该语句,并打开launchSettings.launchUrl
c.DocExpansion(Swashbuckle.AspNetCore.SwaggerUI.DocExpansion.None);//折叠Api
c.OAuthClientId(adminUiOptions.AdminApiConfiguration.OidcSwaggerUIClientId);
c.OAuthAppName(adminUiOptions.AdminApiConfiguration.ApiName);
c.OAuthUsePkce();
//c.DefaultModelsExpandDepth(-1);//不显示Models
if (!adminUiOptions.SwaggerConfiguration.EnableMiniProfiler) return;
// Gets or sets a Stream function for retrieving the swagger-ui page
if (indexStream != null) c.IndexStream = () => indexStream;
c.InjectJavascript("/swagger/mini-profiler.js?v=4.2.22+2.0");
c.InjectStylesheet("/swagger/mini-profiler.css?v=4.2.22+2.0");
});
#endregion Swagger Api文档
}
/// <summary>
/// Program==> SerialLog and Kestral
/// </summary>
/// <param name="args"></param>
/// <param name="host"></param>
/// <param name="webHost"></param>
/// <param name="webApplicationBuilder"></param>
/// <param name="adminUiOptions"></param>
public static void ConfigureProgram(string[] args, IHostBuilder host, IWebHostBuilder webHost, WebApplicationBuilder webApplicationBuilder, AdminUiOptions adminUiOptions)
{
try
{
Log.Logger = new LoggerConfiguration().CreateBootstrapLogger();
// //configure SerialLog
// host.UseSerilog((hostContext, loggerConfig) =>
// {
// loggerConfig
// .ReadFrom.Configuration(hostContext.Configuration)
// .Enrich.WithProperty("ApplicationName", hostContext.HostingEnvironment.ApplicationName)
//#if DEBUG
// .WriteTo.Console();
//#endif
// });
//配置Kestrel服务器
webHost
.ConfigureAppConfiguration((hostContext, configApp) =>
{
var configurationRoot = configApp.Build();
configApp.AddJsonFile("configs/appsettings.json", optional: true, reloadOnChange: true);
configApp.AddJsonFile("configs/serilog.json", optional: true, reloadOnChange: true);
configApp.AddJsonFile("seeds/datadictionary.json", optional: true, reloadOnChange: true);
configApp.AddJsonFile("seeds/identitydata.json", optional: true, reloadOnChange: true);
configApp.AddJsonFile("seeds/swagger.json", optional: true, reloadOnChange: true);
var env = hostContext.HostingEnvironment;
configApp.AddJsonFile($"configs/appsettings.{env.EnvironmentName}.json", optional: true,
reloadOnChange: true);
configApp.AddJsonFile($"configs/serilog.{env.EnvironmentName}.json", optional: true,
reloadOnChange: true);
configApp.AddEnvironmentVariables();
configApp.AddCommandLine(args);
})
//https://docs.microsoft.com/zh-cn/dotnet/api/microsoft.aspnetcore.http.features.formoptions?view=aspnetcore-6.0
.ConfigureKestrel((context, options) =>
{
//https://docs.microsoft.com/zh-cn/dotnet/api/microsoft.aspnetcore.mvc.requestformlimitsattribute?view=aspnetcore-6.0
options.Limits.MaxConcurrentConnections = 100;
options.Limits.MaxConcurrentUpgradedConnections = 100;
//设置应用服务器Kestrel请求体最大为100MB
options.Limits.MaxRequestBodySize = int.MaxValue;
});
}
catch (Exception ex)
{
Log.Fatal(ex, "Host terminated unexpectedly");
}
finally
{
Log.CloseAndFlush();
}
}
}
}