添加项目文件。
This commit is contained in:
1 parent
6a017c9547
commit
58abbb5b4f
86 files changed
+8729
No files matched your search
@@ -0,0 +1,27 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace StoreHub.Framework.DependencyInjection
|
||||
{
|
||||
public static class FreeSqlDB
|
||||
{
|
||||
public static IServiceCollection AddFreeSqlDB(this IServiceCollection services)
|
||||
{
|
||||
Func<IServiceProvider, IFreeSql> fsqlFactory = r =>
|
||||
{
|
||||
var config = r.GetRequiredService<IConfiguration>();
|
||||
var connection = config.GetConnectionString("DefaultConnection");
|
||||
IFreeSql fsql = new FreeSql.FreeSqlBuilder()
|
||||
.UseConnectionString(FreeSql.DataType.MySql, connection)
|
||||
.UseAdoConnectionPool(true)
|
||||
.Build();
|
||||
return fsql;
|
||||
};
|
||||
services.AddSingleton<IFreeSql>(fsqlFactory);
|
||||
return services;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
using Asp.Versioning.OpenApi;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using Microsoft.OpenApi;
|
||||
using StoreHub.Framework.Security;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
|
||||
namespace StoreHub.Framework.DependencyInjection;
|
||||
|
||||
/// <summary>
|
||||
/// JWT 认证服务注册入口(由宿主 Program.cs 调用)。
|
||||
/// </summary>
|
||||
public static class JwtServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 注册 JWT 认证:
|
||||
/// <list type="number">
|
||||
/// <item>绑定 <c>Jwt</c> 配置节到 <see cref="JwtOptions"/>,并在启动时校验签名密钥长度;</item>
|
||||
/// <item>注册 <see cref="ITokenService"/>(令牌生成/校验)与 <see cref="ICurrentUser"/>(当前用户访问器);</item>
|
||||
/// <item>注册 <see cref="JwtBearerDefaults"/> 认证,认证失败/授权失败统一返回 <see cref="StoreHub.Framework.Commons.ApiResult{T}"/> 格式;</item>
|
||||
/// <item>注册 <see cref="IOnlineUserStore"/>(基于 <c>IMemoryCache</c> 的在线会话存储):单点登录、在线人员查询、退出销毁;</item>
|
||||
/// <item>为 OpenAPI 文档声明 Bearer(JWT) 安全方案,并对标记了 [Authorize] 的接口声明需要携带令牌。</item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 需要在请求管道中调用 <c>app.UseAuthentication()</c>(位于 <c>app.UseAuthorization()</c> 之前)。
|
||||
/// </remarks>
|
||||
public static IServiceCollection AddStoreHubJwt(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
ArgumentNullException.ThrowIfNull(configuration);
|
||||
|
||||
// JwtOptions:绑定 Jwt 配置节,启动时校验 HS256 签名密钥长度
|
||||
services.AddOptions<JwtOptions>()
|
||||
.Bind(configuration.GetSection(JwtOptions.SectionName))
|
||||
.Validate(
|
||||
o => !string.IsNullOrWhiteSpace(o.SigningKey)
|
||||
&& Encoding.UTF8.GetByteCount(o.SigningKey) >= 32,
|
||||
"Jwt:SigningKey 长度不足,HS256 签名密钥至少需要 32 字节(256位)。")
|
||||
.ValidateOnStart();
|
||||
|
||||
services.AddHttpContextAccessor();
|
||||
|
||||
// 令牌服务与当前用户访问器
|
||||
services.AddSingleton<ITokenService, TokenService>();
|
||||
services.AddScoped<ICurrentUser, CurrentUser>();
|
||||
|
||||
// 在线会话存储(单点登录):基于 IMemoryCache 的进程内缓存,用于在线人员管理与退出销毁
|
||||
services.AddMemoryCache();
|
||||
services.AddSingleton<IOnlineUserStore, OnlineUserStore>();
|
||||
|
||||
// JwtBearer 认证
|
||||
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||
.AddJwtBearer(options =>
|
||||
{
|
||||
JwtOptions jwt = configuration.GetSection(JwtOptions.SectionName).Get<JwtOptions>() ?? new JwtOptions();
|
||||
options.TokenValidationParameters = jwt.ToTokenValidationParameters();
|
||||
options.Events = new JwtBearerEvents
|
||||
{
|
||||
OnChallenge = JwtBearerResponseHandler.OnChallenge,
|
||||
OnForbidden = JwtBearerResponseHandler.OnForbidden,
|
||||
// 单点登录校验:会话必须仍有效(未退出、未被新登录顶替)
|
||||
OnTokenValidated = context =>
|
||||
{
|
||||
IOnlineUserStore onlineUserStore = context.HttpContext.RequestServices.GetRequiredService<IOnlineUserStore>();
|
||||
ClaimsPrincipal? principal = context.Principal;
|
||||
string? userIdValue = principal?.FindFirst(JwtClaimTypes.UserId)?.Value;
|
||||
string? jti = principal?.FindFirst(JwtClaimTypes.Jti)?.Value;
|
||||
|
||||
if (!int.TryParse(userIdValue, out int userId)
|
||||
|| string.IsNullOrWhiteSpace(jti)
|
||||
|| !onlineUserStore.IsTokenActive(userId, jti))
|
||||
{
|
||||
context.Fail(new SecurityTokenException("登录已失效,请重新登录"));
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// OpenAPI:声明 Bearer(JWT) 安全方案(Scalar 页面即可显示 Authorize/输入 token),
|
||||
// 并仅对标记了 [Authorize] 的接口声明需要携带令牌(与宿主的 AddOpenApi 配置合并生效)
|
||||
services.Configure<VersionedOpenApiOptions>(options =>
|
||||
{
|
||||
// 在 OpenAPI 文档中声明 Bearer(JWT) 安全方案
|
||||
options.Document.AddDocumentTransformer((document, _, _) =>
|
||||
{
|
||||
document.Components ??= new OpenApiComponents();
|
||||
document.Components.SecuritySchemes ??= new Dictionary<string, IOpenApiSecurityScheme>();
|
||||
document.Components.SecuritySchemes.TryAdd("Bearer", new OpenApiSecurityScheme
|
||||
{
|
||||
Type = SecuritySchemeType.Http,
|
||||
Scheme = "bearer",
|
||||
BearerFormat = "JWT",
|
||||
Description = "登录接口返回的 accessToken,直接粘贴令牌内容即可(无需手动加 Bearer 前缀)。",
|
||||
});
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
|
||||
// 仅对标记了 [Authorize] 的接口声明需要携带令牌
|
||||
options.Document.AddOperationTransformer((operation, context, _) =>
|
||||
{
|
||||
IList<object> metadata = context.Description.ActionDescriptor.EndpointMetadata;
|
||||
bool requiresAuth = metadata.OfType<IAuthorizeData>().Any()
|
||||
&& !metadata.OfType<IAllowAnonymous>().Any();
|
||||
if (requiresAuth)
|
||||
{
|
||||
operation.Security =
|
||||
[
|
||||
new OpenApiSecurityRequirement
|
||||
{
|
||||
[new OpenApiSecuritySchemeReference("Bearer", context.Document, "")] = [],
|
||||
},
|
||||
];
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
using MAX.Core.DependencyInjection;
|
||||
using MAX.Core.Infrastructure;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System.Diagnostics;
|
||||
using System.Reflection;
|
||||
|
||||
namespace StoreHub.Framework.DependencyInjection
|
||||
{
|
||||
/// <summary>
|
||||
/// IOC 注册入口(由宿主 Program.cs 调用)
|
||||
/// </summary>
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
private static readonly Type[] DependencyMarkers =
|
||||
[
|
||||
typeof(ISingletonDependency),
|
||||
typeof(IScopedDependency),
|
||||
typeof(ITransientDependency),
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// 注册应用服务:
|
||||
/// <list type="number">
|
||||
/// <item>自动注册所有实现 <see cref="ISingletonDependency"/> / <see cref="IScopedDependency"/> / <see cref="ITransientDependency"/> 的类型;</item>
|
||||
/// <item>发现所有 <see cref="IStartup"/>,按 <see cref="IStartup.Priority"/> 降序执行 Initialize + Register。</item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public static IServiceCollection AddStoreHub(this IServiceCollection services)
|
||||
{
|
||||
IEnumerable<Assembly> assemblies = GetApplicationAssemblies();
|
||||
|
||||
RegisterDependencyMarkers(services, assemblies);
|
||||
RunStartups(services, assemblies);
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 自动注册标记接口(ISingletonDependency / IScopedDependency / ITransientDependency)的实现类型
|
||||
/// </summary>
|
||||
private static void RegisterDependencyMarkers(IServiceCollection services, IEnumerable<Assembly> assemblies)
|
||||
{
|
||||
foreach (Type type in GetConcreteTypes(assemblies))
|
||||
{
|
||||
Debug.WriteLine(type.Namespace);
|
||||
Type? marker = DependencyMarkers.FirstOrDefault(m => m.IsAssignableFrom(type));
|
||||
if (marker is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
ServiceLifetime lifetime = marker == typeof(ISingletonDependency) ? ServiceLifetime.Singleton
|
||||
: marker == typeof(IScopedDependency) ? ServiceLifetime.Scoped
|
||||
: ServiceLifetime.Transient;
|
||||
|
||||
// 注册为自身
|
||||
services.Add(new ServiceDescriptor(type, type, lifetime));
|
||||
|
||||
// 注册为实现的接口(排除标记接口与系统接口)
|
||||
foreach (Type iface in type.GetInterfaces())
|
||||
{
|
||||
if (DependencyMarkers.Contains(iface) || IsSystemInterface(iface))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
services.Add(new ServiceDescriptor(iface, type, lifetime));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发现所有 <see cref="IStartup"/> 实现,按 <see cref="IStartup.Priority"/> 降序执行
|
||||
/// </summary>
|
||||
private static void RunStartups(IServiceCollection services, IEnumerable<Assembly> assemblies)
|
||||
{
|
||||
IServiceRegistrar registrar = new ServiceRegistrarProxy(services);
|
||||
|
||||
foreach (IStartup startup in GetConcreteTypes(assemblies)
|
||||
.Where(t => typeof(IStartup).IsAssignableFrom(t))
|
||||
.Select(t => (IStartup)Activator.CreateInstance(t, true)!)
|
||||
.OrderByDescending(s => s.Priority))
|
||||
{
|
||||
startup.Initialize();
|
||||
startup.Register(registrar);
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<Type> GetConcreteTypes(IEnumerable<Assembly> assemblies)
|
||||
{
|
||||
return assemblies
|
||||
.SelectMany(SafeGetTypes)
|
||||
.Where(t => t.IsClass && !t.IsAbstract && !t.IsGenericTypeDefinition);
|
||||
}
|
||||
|
||||
private static IEnumerable<Type> SafeGetTypes(Assembly assembly)
|
||||
{
|
||||
try
|
||||
{
|
||||
return assembly.GetTypes();
|
||||
}
|
||||
catch (ReflectionTypeLoadException ex)
|
||||
{
|
||||
return ex.Types.OfType<Type>();
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<Assembly> GetApplicationAssemblies()
|
||||
{
|
||||
// 使用 AppTypeFinder:扫描应用输出目录 + 已加载程序集,
|
||||
return StoreHubAppTypeFinder.Current.GetAssemblies();
|
||||
}
|
||||
|
||||
private static bool IsSystemInterface(Type type)
|
||||
{
|
||||
return type.Namespace is null
|
||||
|| type.Namespace.StartsWith("System", StringComparison.Ordinal)
|
||||
|| type.Namespace.StartsWith("Microsoft", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
using MAX.Core.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace StoreHub.Framework.DependencyInjection
|
||||
{
|
||||
/// <summary>
|
||||
/// 将 <see cref="IServiceRegistrar"/> 适配到 Microsoft.Extensions.DependencyInjection
|
||||
/// </summary>
|
||||
internal sealed class ServiceRegistrarProxy : IServiceRegistrar
|
||||
{
|
||||
private readonly IServiceCollection _services;
|
||||
|
||||
public ServiceRegistrarProxy(IServiceCollection services)
|
||||
{
|
||||
_services = services;
|
||||
}
|
||||
|
||||
public void RegisterSingleton<TService, TImplementation>() where TService : class where TImplementation : class, TService
|
||||
{
|
||||
_services.AddSingleton<TService, TImplementation>();
|
||||
}
|
||||
|
||||
public void RegisterSingleton<TService>(TService instance) where TService : class
|
||||
{
|
||||
_services.AddSingleton(instance);
|
||||
}
|
||||
|
||||
public void RegisterSingleton<TService>(Func<IServiceProvider, TService> factory) where TService : class
|
||||
{
|
||||
_services.AddSingleton(factory);
|
||||
}
|
||||
|
||||
public void RegisterSingleton<TService, TImplementation>(object? serviceKey) where TService : class where TImplementation : class, TService
|
||||
{
|
||||
_services.AddKeyedSingleton<TService, TImplementation>(serviceKey);
|
||||
}
|
||||
|
||||
public void RegisterSingleton<TService>(object? serviceKey, TService instance) where TService : class
|
||||
{
|
||||
_services.AddKeyedSingleton(serviceKey, instance);
|
||||
}
|
||||
|
||||
public void RegisterSingleton<TService>(object? serviceKey, Func<IServiceProvider, object?, TService> factory) where TService : class
|
||||
{
|
||||
_services.AddKeyedSingleton(serviceKey, factory);
|
||||
}
|
||||
|
||||
public void RegisterScoped<TService, TImplementation>() where TService : class where TImplementation : class, TService
|
||||
{
|
||||
_services.AddScoped<TService, TImplementation>();
|
||||
}
|
||||
|
||||
public void RegisterScoped<TService>(Func<IServiceProvider, TService> factory) where TService : class
|
||||
{
|
||||
_services.AddScoped(factory);
|
||||
}
|
||||
|
||||
public void RegisterScoped<TService, TImplementation>(object? serviceKey) where TService : class where TImplementation : class, TService
|
||||
{
|
||||
_services.AddKeyedScoped<TService, TImplementation>(serviceKey);
|
||||
}
|
||||
|
||||
public void RegisterScoped<TService>(object? serviceKey, Func<IServiceProvider, object?, TService> factory) where TService : class
|
||||
{
|
||||
_services.AddKeyedScoped(serviceKey, factory);
|
||||
}
|
||||
|
||||
public void RegisterTransient<TService, TImplementation>() where TService : class where TImplementation : class, TService
|
||||
{
|
||||
_services.AddTransient<TService, TImplementation>();
|
||||
}
|
||||
|
||||
public void RegisterTransient<TService>(Func<IServiceProvider, TService> factory) where TService : class
|
||||
{
|
||||
_services.AddTransient(factory);
|
||||
}
|
||||
|
||||
public void RegisterTransient<TService, TImplementation>(object? serviceKey) where TService : class where TImplementation : class, TService
|
||||
{
|
||||
_services.AddKeyedTransient<TService, TImplementation>(serviceKey);
|
||||
}
|
||||
|
||||
public void RegisterTransient<TService>(object? serviceKey, Func<IServiceProvider, object?, TService> factory) where TService : class
|
||||
{
|
||||
_services.AddKeyedTransient(serviceKey, factory);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using MAX.Core.Reflections;
|
||||
|
||||
namespace StoreHub.Framework.DependencyInjection
|
||||
{
|
||||
/// <summary>
|
||||
/// 本应用的类型查找器:在 AppTypeFinder 默认前缀(MAX. / Nexus.)之外,
|
||||
/// 增加 StoreHub、KJD 前缀,用于发现宿主和各业务模块的程序集。
|
||||
/// </summary>
|
||||
internal sealed class StoreHubAppTypeFinder : AppTypeFinder
|
||||
{
|
||||
public static StoreHubAppTypeFinder Current { get; } = new();
|
||||
|
||||
protected override List<string> AvailablePrefix()
|
||||
{
|
||||
return ["StoreHub"];
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user