增加Shop API
This commit is contained in:
1 parent
3769193696
commit
5d017740c6
83 files changed
+4451
-696
No files matched your search
@@ -1,151 +0,0 @@
|
||||
using Asp.Versioning.OpenApi;
|
||||
using MAX.Framework.Security;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using Microsoft.OpenApi;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
|
||||
namespace MAX.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="Dtos.ApiResult{T}"/> 格式;</item>
|
||||
/// <item>注册 <see cref="IOnlineUserStore"/>(数据库会话存储):多端登录、会话查询、退出撤销;</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>();
|
||||
|
||||
// 使用宿主注册的 IFreeSql 持久化会话。
|
||||
|
||||
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,
|
||||
OnMessageReceived = context =>
|
||||
{
|
||||
// 固定测试凭据只在 Development 环境生效,不依赖 JWT 过期时间或在线会话。
|
||||
var environment = context.HttpContext.RequestServices.GetRequiredService<IHostEnvironment>();
|
||||
var development = configuration.GetSection("Jwt:DevelopmentToken");
|
||||
var token = development["Token"];
|
||||
var userName = development["UserName"];
|
||||
var authorization = context.Request.Headers.Authorization.ToString();
|
||||
if (environment.IsDevelopment()
|
||||
&& development.GetValue<bool>("Enabled")
|
||||
&& !string.IsNullOrWhiteSpace(token)
|
||||
&& !string.IsNullOrWhiteSpace(userName)
|
||||
&& authorization.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)
|
||||
&& string.Equals(authorization[7..].Trim(), token, StringComparison.Ordinal))
|
||||
{
|
||||
context.Principal = new ClaimsPrincipal(new ClaimsIdentity(
|
||||
[new Claim(JwtClaimTypes.UserName, userName),
|
||||
new Claim(JwtClaimTypes.NickName, "开发测试用户")],
|
||||
JwtBearerDefaults.AuthenticationScheme));
|
||||
context.Success();
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
},
|
||||
// 会话校验:必须未过期且未撤销
|
||||
OnTokenValidated = context =>
|
||||
{
|
||||
IOnlineUserStore onlineUserStore = context.HttpContext.RequestServices.GetRequiredService<IOnlineUserStore>();
|
||||
ClaimsPrincipal? principal = context.Principal;
|
||||
string? userName = principal?.FindFirst(JwtClaimTypes.UserName)?.Value;
|
||||
string? jti = principal?.FindFirst(JwtClaimTypes.Jti)?.Value;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(userName)
|
||||
|| string.IsNullOrWhiteSpace(jti)
|
||||
|| !onlineUserStore.IsTokenActive(userName, 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,72 @@
|
||||
using Asp.Versioning.OpenApi;
|
||||
using MAX.Framework.Security;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.OpenApi;
|
||||
|
||||
namespace MAX.Framework.DependencyInjection;
|
||||
|
||||
public static class TokenAuthServiceCollectionExtensions
|
||||
{
|
||||
// Compatibility entry point; authentication no longer uses JWT.
|
||||
public static IServiceCollection AddStoreHubJwt(this IServiceCollection services, IConfiguration configuration)
|
||||
=> services.AddStoreHubTokenAuth(configuration);
|
||||
|
||||
public static IServiceCollection AddStoreHubTokenAuth(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
services.AddOptions<TokenAuthOptions>().Bind(configuration.GetSection("TokenAuth"))
|
||||
.Validate(o => o.IdleDays > 0 && o.IdleDays <= 36500 && o.AbsoluteMaxDays > 0 && o.AbsoluteMaxDays <= 36500
|
||||
&& o.RenewIntervalMinutes > 0 && o.RenewIntervalMinutes < o.IdleDays * 1440
|
||||
&& o.CacheSeconds >= 0 && o.CacheSeconds <= 86400 && o.MaxDevicesPerUser >= 0,
|
||||
"TokenAuth 配置无效:有效期必须为正,续期间隔必须小于闲置有效期,缓存和设备数不能为负。")
|
||||
.ValidateOnStart();
|
||||
services.AddMemoryCache();
|
||||
services.AddSingleton(TimeProvider.System);
|
||||
services.AddHttpContextAccessor();
|
||||
services.AddSingleton<TokenService>();
|
||||
services.AddSingleton<ITokenService>(sp => sp.GetRequiredService<TokenService>());
|
||||
services.AddSingleton<IOnlineUserStore, OnlineUserStore>();
|
||||
services.AddScoped<ICurrentUser, CurrentUser>();
|
||||
services.AddAuthentication("Bearer").AddScheme<AuthenticationSchemeOptions, TokenAuthenticationHandler>("Bearer", _ => { });
|
||||
services.Configure<VersionedOpenApiOptions>(options =>
|
||||
{
|
||||
// 在 OpenAPI 文档中声明 Bearer 安全方案
|
||||
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 = "Opaque",
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user