JKFZJCXT/BZPT.Api/Program.cs

455 lines
17 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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();