455 lines
17 KiB
C#
455 lines
17 KiB
C#
using Autofac;
|
||
using Autofac.Extensions.DependencyInjection;
|
||
using Microsoft.AspNetCore.Builder;
|
||
using Microsoft.AspNetCore.Hosting;
|
||
using Microsoft.AspNetCore.Mvc;
|
||
using Microsoft.AspNetCore.Mvc.Controllers;
|
||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||
using Microsoft.Extensions.DependencyInjection;
|
||
using Microsoft.Extensions.Hosting;
|
||
using Microsoft.Extensions.Logging;
|
||
using Microsoft.OpenApi.Models;
|
||
using System;
|
||
using System.IO;
|
||
using System.Text;
|
||
using System.Threading.Tasks;
|
||
using NPlatform.Infrastructure.Config;
|
||
using NPlatform.Middleware;
|
||
using NPlatform.API;
|
||
using NPlatform.DI;
|
||
using NPlatform.Repositories;
|
||
using Newtonsoft.Json;
|
||
using System.Text.Json.Serialization;
|
||
using SqlSugar;
|
||
using IGeekFan.AspNetCore.Knife4jUI;
|
||
using Microsoft.AspNetCore.Authentication;
|
||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||
using IdentityServer4.Configuration;
|
||
using System.Security.Cryptography.X509Certificates;
|
||
using BZPT.Repositories;
|
||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||
using NPlatform.Infrastructure.Config.Section;
|
||
using ServiceStack;
|
||
using Microsoft.IdentityModel.Tokens;
|
||
using Hangfire;
|
||
using Hangfire.Redis.StackExchange;
|
||
using Hangfire.Dashboard.BasicAuthorization;
|
||
using BZPT.Domains.Application;
|
||
using System.Security.Claims;
|
||
using NPlatform.Infrastructure.IdGenerators;
|
||
using NPOI.SS.Formula.Functions;
|
||
using Microsoft.AspNetCore.DataProtection;
|
||
using StackExchange.Redis;
|
||
using BZPT.Api.Middleware;
|
||
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
||
using Newtonsoft.Json.Linq;
|
||
using Serilog.Context;
|
||
using Serilog;
|
||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||
using Microsoft.Extensions.FileProviders;
|
||
using BZPT.Domains.IRepositories;
|
||
using BZPT.Domains.IService.Sys;
|
||
using BZPT.Api;
|
||
using Microsoft.AspNetCore.Http.Features;
|
||
using Microsoft.AspNetCore.HttpOverrides;
|
||
|
||
var builder = WebApplication.CreateBuilder(args);
|
||
var serviceConfig = builder.Configuration.GetServiceConfig();
|
||
|
||
builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());
|
||
// 替换控制器激活器
|
||
builder.Services.Replace(ServiceDescriptor.Scoped<IControllerActivator, ServiceBasedControllerActivator>());
|
||
|
||
// 健康检查
|
||
builder.Services.AddHealthChecks().AddCheck<NHealthChecks>(serviceConfig.ServiceName);
|
||
// 内存缓存
|
||
builder.Services.AddMemoryCache();
|
||
// Autofac 配置
|
||
builder.Services.AddAutofac();
|
||
var redisConfig = builder.Configuration.GetRedisConfig();
|
||
//从配置获取 Redis 连接字符串
|
||
var redisConn = $"{redisConfig.Connections?.FirstOrDefault()},password={redisConfig.Password}";
|
||
Console.WriteLine("redis 连接:" + redisConn);
|
||
// 配置 Hangfire
|
||
builder.Services.AddHangfire(x =>
|
||
{
|
||
x.SetDataCompatibilityLevel(CompatibilityLevel.Version_170)
|
||
.UseSimpleAssemblyNameTypeSerializer()
|
||
.UseRecommendedSerializerSettings()
|
||
.UseRedisStorage(redisConn, new RedisStorageOptions
|
||
{
|
||
Db = 11,
|
||
Prefix = "Hangfire:"
|
||
});
|
||
});
|
||
|
||
builder.Services.AddHangfireServer();
|
||
|
||
// 连接到 Redis
|
||
var connectionMultiplexer = ConnectionMultiplexer.Connect(redisConn);
|
||
// 注册 IConnectionMultiplexer
|
||
builder.Services.AddSingleton<IConnectionMultiplexer>(connectionMultiplexer);
|
||
|
||
// 注册 IDatabase
|
||
builder.Services.AddScoped<IDatabase>(provider =>
|
||
{
|
||
var connection = provider.GetRequiredService<IConnectionMultiplexer>();
|
||
return connection.GetDatabase(); // 返回 IDatabase 实例
|
||
});
|
||
// 主机配置
|
||
var repositoryOptions = new RepositoryOptions();
|
||
builder.Host.Configure(builder.Configuration, repositoryOptions);
|
||
|
||
// 数据库上下文配置
|
||
var dbHost = builder.Configuration["DB_HOST"];
|
||
var dbPort = builder.Configuration["DB_PORT"];
|
||
var dbName = builder.Configuration["DB_NAME"];
|
||
var logDbName = builder.Configuration["LOG_DB_NAME"];
|
||
var dbUser = builder.Configuration["DB_USER"];
|
||
var dbPass = builder.Configuration["DB_PASSWORD"];
|
||
|
||
builder.UseMySerilog($"Server={dbHost};Port={dbPort};DATABASE={logDbName};User Id={dbUser};PWD={dbPass};", serviceConfig.ServiceName);
|
||
|
||
//// 配置日志
|
||
//var log = builder.Logging.AddLog4Net();
|
||
//log.AddConsole();
|
||
//log.AddDebug();
|
||
//log.SetMinimumLevel(LogLevel.Debug);
|
||
|
||
builder.Services.AddScoped<DBContext>(serviceProvider =>
|
||
{
|
||
return new DBContext($"Server={dbHost};Port={dbPort};DATABASE={dbName};User Id={dbUser};PWD={dbPass};", DbType.Dm, ConfigId: "default");
|
||
});
|
||
builder.Services.AddScoped<IUnitOfWorkSugar, UnitOfWorkSugar>();
|
||
// 控制器配置
|
||
builder.Services.AddControllersWithViews(mvcOptions =>
|
||
{
|
||
mvcOptions.Filters.Remove(mvcOptions.Filters.OfType<UnsupportedContentTypeFilter>().FirstOrDefault());
|
||
}).AddJsonOptions(options =>
|
||
{
|
||
//options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
|
||
//options.JsonSerializerOptions.PropertyNameCaseInsensitive = false; // 确保区分大小写
|
||
options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
|
||
options.JsonSerializerOptions.Converters.Add(new DateTimeConverter("yyyy-MM-dd HH:mm:ss"));
|
||
});
|
||
|
||
|
||
// 从 appsettings.json 加载表单限制
|
||
builder.Services.Configure<FormOptions>(options =>
|
||
{
|
||
options.MultipartBodyLengthLimit =
|
||
builder.Configuration.GetValue<long>("FormOptions:MultipartBodyLengthLimit");
|
||
});
|
||
|
||
|
||
// API 行为配置
|
||
builder.Services.Configure<ApiBehaviorOptions>(options =>
|
||
{
|
||
options.InvalidModelStateResponseFactory = actionContext =>
|
||
{
|
||
var errors = actionContext.ModelState?.Where(e => e.Value.Errors.Count > 0);
|
||
var strError = new StringBuilder();
|
||
foreach (var error in errors)
|
||
{
|
||
var msg = error.Value.Errors.FirstOrDefault()?.ErrorMessage;
|
||
if (!string.IsNullOrWhiteSpace(msg))
|
||
strError.Append(msg);
|
||
}
|
||
return new FailResult<object>("错误:" + strError.ToString());
|
||
};
|
||
});
|
||
|
||
// IdentityServer4 配置
|
||
var serviceProvider = builder.Services.BuildServiceProvider();
|
||
var dbContext = serviceProvider.GetService<DBContext>();
|
||
|
||
var keyPath = Path.Combine(Environment.CurrentDirectory, builder.Configuration["CERT_PATH"] ?? "");
|
||
Console.WriteLine($"Certificate path: {keyPath}, Exists: {File.Exists(keyPath)}");
|
||
|
||
// CORS 配置
|
||
|
||
builder.Services.AddCors(options => options.AddPolicy("CorsMy", policy =>
|
||
{
|
||
// 允许特定的源访问
|
||
policy.WithOrigins("*")
|
||
.AllowAnyHeader()
|
||
.AllowAnyMethod();
|
||
|
||
}));
|
||
|
||
if (!string.IsNullOrEmpty(keyPath) && File.Exists(keyPath))
|
||
{
|
||
builder.WebHost.ConfigureKestrel(serverOptions =>
|
||
{
|
||
serverOptions.ConfigureHttpsDefaults(httpsOptions =>
|
||
{
|
||
httpsOptions.ServerCertificate = new X509Certificate2(keyPath, builder.Configuration["CERT_PASSWORD"]);
|
||
});
|
||
});
|
||
}
|
||
else
|
||
{
|
||
Console.WriteLine("⚠️ 证书未找到或环境变量未设置,HTTPS 可能无法正常工作!");
|
||
}
|
||
|
||
|
||
Console.WriteLine($"AuthorityServer 配置: {builder.Configuration["AuthorityServer"]}");
|
||
// 配置 Cookie 认证
|
||
builder.Services.AddAuthentication(options =>
|
||
{
|
||
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
|
||
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
|
||
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
|
||
options.DefaultSignOutScheme = CookieAuthenticationDefaults.AuthenticationScheme;
|
||
})
|
||
.AddJwtBearer(options =>
|
||
{
|
||
options.Authority = builder.Configuration["AuthorityServer"];
|
||
options.Audience = serviceConfig.ServiceID; // 受众
|
||
#if DEBUG
|
||
options.RequireHttpsMetadata = false; // 开发环境允许 HTTP
|
||
#else
|
||
options.RequireHttpsMetadata = true; // 生产环境强制 HTTPS
|
||
#endif
|
||
options.TokenValidationParameters = new TokenValidationParameters
|
||
{
|
||
ValidateAudience = true, // 验证 Audience
|
||
ValidAudiences =new string[] { serviceConfig.ServiceID }, // 或者 "api.BZPT",取决于你的 API 需要验证哪个 Audience
|
||
ValidateIssuer = true, // 验证 Issuer
|
||
ValidIssuer = builder.Configuration["AuthorityServer"], // 确保与 IdentityServer4 的 Issuer 配置一致
|
||
ValidateLifetime = true,
|
||
ClockSkew = TimeSpan.Zero // 生产环境建议设为小值或 TimeSpan.Zero,防止 Token 刚签发就被认为过期
|
||
};
|
||
options.BackchannelHttpHandler = new HttpClientHandler
|
||
{
|
||
ServerCertificateCustomValidationCallback = (message, cert, chain, sslPolicyErrors) => true // 忽略 SSL 错误
|
||
};
|
||
|
||
options.Events = new JwtBearerEvents
|
||
{
|
||
OnAuthenticationFailed = context =>
|
||
{
|
||
Console.WriteLine($"{serviceConfig.ServiceID}|{builder.Configuration["AuthorityServer"]} ");
|
||
// 记录身份验证失败的详细信息
|
||
Console.WriteLine($"Authentication failed:{context.Exception.Message}");
|
||
return Task.CompletedTask;
|
||
},
|
||
OnTokenValidated = context =>
|
||
{
|
||
// 记录 token 验证成功的信息
|
||
Console.WriteLine("Token validated successfully.");
|
||
////打印 Claims 信息以便调试
|
||
if (context.Principal != null)
|
||
{
|
||
Console.WriteLine($"Authenticated User Name: {context.Principal.Identity?.Name}");
|
||
foreach (var claim in context.Principal.Claims)
|
||
{
|
||
Console.WriteLine($" Claim Type: {claim.Type}, Value: {claim.Value}");
|
||
}
|
||
}
|
||
return Task.CompletedTask;
|
||
},
|
||
OnMessageReceived = context =>
|
||
{
|
||
//Console.WriteLine("--- JwtBearer OnMessageReceived triggered ---");
|
||
if (context.Request.Headers.ContainsKey("Authorization"))
|
||
{
|
||
Console.WriteLine($"Raw Authorization Header: {context.Request.Headers["Authorization"]}");
|
||
}
|
||
// 这里的 context.Token 应该已经由 JwtBearerHandler 填充,除非有其他中间件干扰
|
||
Console.WriteLine($"Received token (JwtBearer): {context.Token ?? "NULL"}"); // 确保打印 NULL
|
||
Console.WriteLine("--- End JwtBearer OnMessageReceived ---");
|
||
return Task.CompletedTask;
|
||
},
|
||
OnChallenge = context =>
|
||
{
|
||
Console.WriteLine("JwtBearer Challenge triggered.");
|
||
Console.WriteLine($"Challenge Scheme: {context.Scheme.Name}");
|
||
Console.WriteLine($"Challenge Status Code: {context.Response.StatusCode}");
|
||
if (!string.IsNullOrEmpty(context.Error)) Console.WriteLine($"Challenge Error: {context.Error}");
|
||
if (!string.IsNullOrEmpty(context.ErrorDescription)) Console.WriteLine($"Challenge Error Description: {context.ErrorDescription}");
|
||
//全自定义 401 响应,可以在这里处理
|
||
// context.HandleResponse(); // 阻止默认的 401 行为
|
||
// context.Response.StatusCode = StatusCodes.Status401Unauthorized;
|
||
// context.Response.ContentType = "application/json";
|
||
// return context.Response.WriteAsync(JsonSerializer.Serialize(new { message = "Unauthorized", detail = context.ErrorDescription }));
|
||
return Task.CompletedTask;
|
||
}
|
||
};
|
||
});
|
||
|
||
// Swagger 配置
|
||
builder.Services.AddSwaggerGen(c =>
|
||
{
|
||
c.SwaggerDoc(serviceConfig.ServiceName, new OpenApiInfo { Title = $"{serviceConfig.ServiceName} 接口文档", Version = serviceConfig.ServiceVersion });
|
||
c.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, "BZPT.Api.xml"), true);
|
||
|
||
c.AddSecurityRequirement(new OpenApiSecurityRequirement
|
||
{
|
||
{
|
||
new OpenApiSecurityScheme {
|
||
Reference = new OpenApiReference
|
||
{
|
||
Type = ReferenceType.SecurityScheme,
|
||
Id = "Bearer"
|
||
} ,
|
||
Scheme = "oauth2",
|
||
Name = "Bearer",
|
||
In = ParameterLocation.Header,
|
||
}, new List<string>() }
|
||
});
|
||
|
||
// 定义 OAuth2 Client Credentials 安全方案
|
||
c.AddSecurityDefinition("OAuth2", new OpenApiSecurityScheme
|
||
{
|
||
Type = SecuritySchemeType.OAuth2,
|
||
Flows = new OpenApiOAuthFlows
|
||
{
|
||
ClientCredentials = new OpenApiOAuthFlow
|
||
{
|
||
TokenUrl = new Uri($"{builder.Configuration["AuthorityServer"]}/connect/token"), // IdentityServer4 令牌端点
|
||
Scopes = new Dictionary<string, string>
|
||
{
|
||
{ "api_scope", "访问 API 的权限" }
|
||
}
|
||
}
|
||
}
|
||
});
|
||
c.AddServer(new OpenApiServer { Url = "", Description = "vvv" });
|
||
c.CustomOperationIds(apiDesc => (apiDesc.ActionDescriptor as ControllerActionDescriptor)?.ControllerName + "-" + (apiDesc.ActionDescriptor as ControllerActionDescriptor)?.ActionName);
|
||
});
|
||
|
||
var redis = ConnectionMultiplexer.Connect(redisConn);
|
||
builder.Services.AddDataProtection()
|
||
.PersistKeysToStackExchangeRedis(redis, "DataProtection-Keys")
|
||
.SetApplicationName(serviceConfig.ServiceID);
|
||
builder.Services.AddAntiforgery(options =>
|
||
{
|
||
// 可以在这里配置防伪令牌的相关选项
|
||
options.HeaderName = "X-CSRF-TOKEN";
|
||
});
|
||
|
||
//将 Serilog 注册到默认日志工厂(非必须,但更兼容)
|
||
builder.Logging.ClearProviders(); // 可选:移除默认日志提供程序(如不需要控制台重复输出)
|
||
builder.Logging.AddSerilog(dispose: true); // 桥接 Microsoft.Extensions.Logging 到 Serilog
|
||
|
||
// 应用构建
|
||
var app = builder.Build();
|
||
|
||
// 并且在 UseAuthentication() 和 UseAuthorization() 之前。
|
||
app.UseForwardedHeaders(new ForwardedHeadersOptions
|
||
{
|
||
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost
|
||
// 注意:通常不需要 ForwardedHeaders.XForwardedPort,因为 X-Forwarded-Host 包含主机和端口
|
||
// 但如果您的反向代理只设置了 X-Forwarded-Port,则可能需要添加
|
||
|
||
// !!!重要:如果您的反向代理是内部的,并且其IP是固定的,建议指定 KnownProxies 或 KnownNetworks,
|
||
// 以防止伪造的 X-Forwarded-* 头。如果这是在容器内部,且代理是 Ingress Controller 或 Docker 自己的网络,
|
||
// 通常可以安全地跳过 KnownProxies/Networks,但在生产环境中要小心。
|
||
// KnownProxies = { IPAddress.Parse("YOUR_PROXY_IP_HERE") }
|
||
// KnownNetworks = { new IPNetwork(IPAddress.Parse("YOUR_PROXY_NETWORK_START_IP"), YOUR_PROXY_NETWORK_CIDR) }
|
||
});
|
||
|
||
|
||
// 2. 配置 ASP.NET Core 内置异常页(开发环境)
|
||
if (app.Environment.IsDevelopment())
|
||
{
|
||
}
|
||
else
|
||
{
|
||
app.UseHsts();
|
||
}
|
||
// 1. 中间件捕获请求管道中的异常
|
||
app.Use(async (context, next) =>
|
||
{
|
||
try
|
||
{
|
||
#if !DEBUG
|
||
if (DateTime.Now.Hour >= 23 || DateTime.Now.Hour < 7)
|
||
{
|
||
context.Response.StatusCode = 400;
|
||
await context.Response.WriteAsync("系统维护中~");
|
||
return;
|
||
}
|
||
#endif
|
||
// 生成唯一ID(GUID 或 Snowflake ID)
|
||
var requestId = StringObjectIdGenerator.Instance.GenerateId().ToString();
|
||
|
||
// 注入请求头
|
||
context.Request.Headers["X-Request-ID"] = requestId;
|
||
|
||
// 存储到 HttpContext.Items 供后续使用
|
||
context.Items["X-Request-ID"] = requestId;
|
||
// 将 ID 添加到日志上下文
|
||
using (LogContext.PushProperty("RequestID", requestId))
|
||
{
|
||
await next();
|
||
}
|
||
// 处理响应状态码
|
||
if (context.Response.StatusCode == StatusCodes.Status401Unauthorized)
|
||
{
|
||
context.Response.ContentType = "application/json";
|
||
await context.Response.WriteAsync(JsonConvert.SerializeObject(new
|
||
{
|
||
code = 401,
|
||
message = "未授权,请登录"
|
||
}));
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogContext.PushProperty("ErrorCode", 500);
|
||
Serilog.Log.Fatal(ex, "全局捕获的未处理异常(中间件)");
|
||
throw; // 重新抛出以触发内置错误页面
|
||
}
|
||
});
|
||
|
||
// 3. 捕获非 HTTP 管道的全局异常
|
||
AppDomain.CurrentDomain.UnhandledException += (sender, e) =>
|
||
{
|
||
var exception = e.ExceptionObject as Exception;
|
||
LogContext.PushProperty("ErrorCode", 500);
|
||
Serilog.Log.Fatal(exception, "全局捕获的非 HTTP 未处理异常");
|
||
};
|
||
|
||
TaskScheduler.UnobservedTaskException += (sender, e) =>
|
||
{
|
||
LogContext.PushProperty("ErrorCode", 500);
|
||
Serilog.Log.Fatal(e.Exception, "全局捕获的未观察到的任务异常");
|
||
e.SetObserved(); // 标记异常已处理
|
||
};
|
||
|
||
|
||
app.UseSwagger();
|
||
app.UseKnife4UI(c =>
|
||
{
|
||
c.RoutePrefix = "swagger";
|
||
c.SwaggerEndpoint($"/{serviceConfig.ServiceName}/swagger.json", serviceConfig.ServiceName);
|
||
});
|
||
|
||
app.UseHealthChecks("/healthChecks");
|
||
app.UseDefaultFiles(new DefaultFilesOptions { DefaultFileNames = new List<string> { "index.html" } });
|
||
|
||
|
||
app.UseAntiforgery();
|
||
app.UseRouting();
|
||
app.UseCors("CorsMy");
|
||
app.UseAuthentication();
|
||
app.UseAuthorization();
|
||
|
||
//app.UseMiddleware<OrganizationDataFilterMiddleware>();
|
||
// OPTIONS 请求处理
|
||
app.MapMethods("/{**path}", new[] { "OPTIONS" }, () => Results.NoContent()).RequireCors("CorsMy");
|
||
|
||
try
|
||
{
|
||
app.UseIdentityServer();
|
||
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Console.WriteLine($"IdentityServer 中间件加载失败: {ex}");
|
||
}
|
||
|
||
app.MapControllers();
|
||
app.Run(); |