diff --git a/.gitignore b/.gitignore
index 5c35998..11ba6ae 100644
--- a/.gitignore
+++ b/.gitignore
@@ -482,6 +482,6 @@ $RECYCLE.BIN/
*.swp
doc/password.txt
-StopShopping.Api/wwwroot/images/
+wwwroot/
appsettings.json
appsettings.Development.json
\ No newline at end of file
diff --git a/Directory.Build.props b/Directory.Build.props
index 5bee49f..5048c78 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -6,4 +6,11 @@
latest
Recommended
+
+
+
+ Never
+
+
+
\ No newline at end of file
diff --git a/StopShopping.AdminApi/Extensions/AuthExtensions.cs b/StopShopping.AdminApi/Extensions/AuthExtensions.cs
new file mode 100644
index 0000000..4d6ba8a
--- /dev/null
+++ b/StopShopping.AdminApi/Extensions/AuthExtensions.cs
@@ -0,0 +1,56 @@
+using System.Text;
+using Microsoft.AspNetCore.Authentication.JwtBearer;
+using Microsoft.IdentityModel.Tokens;
+using Microsoft.Net.Http.Headers;
+using StopShopping.Services;
+
+namespace StopShopping.AdminApi.Extensions;
+
+public static class JwtExtensions
+{
+ public static IServiceCollection AddAuthServices(this IServiceCollection services, IConfiguration jwtOptions)
+ {
+ services.Configure(jwtOptions);
+ services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
+ .AddJwtBearer(jwtBearerOptions =>
+ {
+ var jwtConfiguration = jwtOptions.Get()!;
+
+ var signingKey = new SymmetricSecurityKey(
+ Encoding.UTF8.GetBytes(jwtConfiguration.SigningKey!)
+ );
+
+ jwtBearerOptions.MapInboundClaims = false;
+ jwtBearerOptions.TokenValidationParameters = new TokenValidationParameters
+ {
+ ValidAudience = jwtConfiguration.ValidAudience,
+ ValidIssuer = jwtConfiguration.ValidIssuer,
+ IssuerSigningKey = signingKey,
+ ClockSkew = TimeSpan.FromSeconds(30) //宽容时间,30秒后才失效
+ };
+ jwtBearerOptions.Events = new JwtBearerEvents
+ {
+ OnMessageReceived = async (context) =>
+ {
+ var accessTokenService = context.HttpContext.RequestServices.GetRequiredService();
+ var authorizationHeader = context.Request.Headers[HeaderNames.Authorization];
+ if (authorizationHeader.Count == 0)
+ {
+ context.Fail($"未找到{HeaderNames.Authorization}请求头");
+ }
+ else
+ {
+ var token = authorizationHeader.First()!.Split(" ").Last();
+ if (string.IsNullOrWhiteSpace(token))
+ context.Fail("未找到token");
+ if (await accessTokenService.IsAccessTokenBlacklistAsync(token))
+ context.Fail("token已失效");
+ }
+ }
+ };
+ });
+ services.AddAuthorization();
+
+ return services;
+ }
+}
\ No newline at end of file
diff --git a/StopShopping.AdminApi/Extensions/BearerOpenApiDocumentTransformer.cs b/StopShopping.AdminApi/Extensions/BearerOpenApiDocumentTransformer.cs
new file mode 100644
index 0000000..5db4caa
--- /dev/null
+++ b/StopShopping.AdminApi/Extensions/BearerOpenApiDocumentTransformer.cs
@@ -0,0 +1,37 @@
+using Microsoft.AspNetCore.Authentication.JwtBearer;
+using Microsoft.AspNetCore.OpenApi;
+using Microsoft.OpenApi;
+
+namespace StopShopping.AdminApi.Extensions;
+
+// 来自网络。。。
+public class BearerOpenApiDocumentTransformer : IOpenApiDocumentTransformer
+{
+ public Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerContext context, CancellationToken cancellationToken)
+ {
+ var bearerOpenApiSecurityScheme = new OpenApiSecurityScheme
+ {
+ In = ParameterLocation.Header,
+ Scheme = JwtBearerDefaults.AuthenticationScheme,
+ Type = SecuritySchemeType.Http,
+ Description = "jwt"
+ };
+
+ document.Components ??= new OpenApiComponents();
+ document.Components.SecuritySchemes ??= new Dictionary();
+ document.Components.SecuritySchemes[JwtBearerDefaults.AuthenticationScheme] = bearerOpenApiSecurityScheme;
+
+ var securityRequirement = new OpenApiSecurityRequirement
+ {
+ {
+ new OpenApiSecuritySchemeReference(JwtBearerDefaults.AuthenticationScheme),
+ new List()
+ }
+ };
+
+ document.Security ??= [];
+ document.Security.Add(securityRequirement);
+
+ return Task.CompletedTask;
+ }
+}
diff --git a/StopShopping.AdminApi/Extensions/CommonServiceCollections.cs b/StopShopping.AdminApi/Extensions/CommonServiceCollections.cs
new file mode 100644
index 0000000..a64d385
--- /dev/null
+++ b/StopShopping.AdminApi/Extensions/CommonServiceCollections.cs
@@ -0,0 +1,76 @@
+using System.Text.Json.Serialization;
+using StopShopping.AdminApi.Middlewares;
+using StopShopping.Services;
+using StopShopping.Services.Extensions;
+
+namespace StopShopping.AdminApi.Extensions;
+
+public static class CommonServiceCollections
+{
+ public static IServiceCollection AddCommonServices(
+ this IServiceCollection services,
+ string corsPolicy,
+ IConfigurationSection jwtConfiguration,
+ IConfigurationSection appConfiguration,
+ bool isDevelopment)
+ {
+ var appOptions = appConfiguration.Get();
+ services.AddCors(options =>
+ {
+ options.AddPolicy(corsPolicy, policy =>
+ {
+ policy.AllowAnyHeader();
+ policy.AllowAnyMethod();
+ policy.WithOrigins(appOptions!.CorsAllowedOrigins);
+ policy.AllowCredentials();
+ });
+ });
+ services.ConfigureHttpJsonOptions(options =>
+ {
+ options.SerializerOptions.Converters.Add(
+ new JsonStringEnumConverter(namingPolicy: null, allowIntegerValues: true));
+ });
+ services.AddHttpContextAccessor();
+ services.AddOpenApi(options =>
+ {
+ options.AddDocumentTransformer();
+ options.AddSchemaTransformer();
+ });
+ services.AddProblemDetails(options =>
+ {
+ options.CustomizeProblemDetails = (context) =>
+ {
+ if (context.ProblemDetails is HttpValidationProblemDetails problemDetails)
+ {
+ problemDetails.AddErrorCode(ProblemDetailsCodes.ParametersValidationFailed);
+ var errors = problemDetails.Errors.Select(e => string.Join(',', e.Value));
+ if (null != errors)
+ problemDetails.Detail = string.Join(',', errors);
+ }
+ };
+ });
+ services.AddValidation();
+ services.AddDistributedMemoryCache();
+
+ services.AddAuthServices(jwtConfiguration);
+
+ services.AddAntiforgery(options =>
+ {
+ var jwtOptions = jwtConfiguration.Get();
+
+ options.HeaderName = appOptions!.CSRFHeaderName;
+ options.Cookie.MaxAge = TimeSpan.FromSeconds(jwtOptions!.RefreshTokenExpiresIn);
+ options.Cookie.HttpOnly = true;
+ options.Cookie.Name = appOptions.CSRFCookieName;
+
+ options.Cookie.SameSite = SameSiteMode.Lax;
+ options.Cookie.Domain = appOptions.CookieDomain;
+ if (!isDevelopment)
+ {
+ options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
+ }
+ });
+
+ return services;
+ }
+}
diff --git a/StopShopping.AdminApi/Extensions/EnumOpenApiSchemaTransformer.cs b/StopShopping.AdminApi/Extensions/EnumOpenApiSchemaTransformer.cs
new file mode 100644
index 0000000..725ffb1
--- /dev/null
+++ b/StopShopping.AdminApi/Extensions/EnumOpenApiSchemaTransformer.cs
@@ -0,0 +1,49 @@
+using System.ComponentModel;
+using System.Text.Json.Nodes;
+using Microsoft.AspNetCore.OpenApi;
+using Microsoft.OpenApi;
+
+namespace StopShopping.AdminApi.Extensions;
+
+///
+/// 处理enum类型openapi显示
+///
+public class EnumOpenApiSchemaTransformer : IOpenApiSchemaTransformer
+{
+ public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext context, CancellationToken cancellationToken)
+ {
+ if (context.JsonTypeInfo.Type.IsEnum)
+ {
+ schema.Type = JsonSchemaType.Integer;
+
+ var enumValues = Enum.GetValues(context.JsonTypeInfo.Type)
+ .Cast