JKFZJCXT/BZPT.Api/Program.cs

455 lines
17 KiB
C#
Raw Normal View History

2025-07-17 09:35:54 +08:00
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
// 生成唯一IDGUID 或 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();