This commit is contained in:
2026-03-30 11:07:30 +08:00
parent 2c44b3a4b2
commit d4a8e71733
74 changed files with 1751 additions and 421 deletions

View File

@@ -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>(jwtOptions);
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(jwtBearerOptions =>
{
var jwtConfiguration = jwtOptions.Get<JwtOptions>()!;
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<IAccessTokenService>();
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;
}
}

View File

@@ -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<string, IOpenApiSecurityScheme>();
document.Components.SecuritySchemes[JwtBearerDefaults.AuthenticationScheme] = bearerOpenApiSecurityScheme;
var securityRequirement = new OpenApiSecurityRequirement
{
{
new OpenApiSecuritySchemeReference(JwtBearerDefaults.AuthenticationScheme),
new List<string>()
}
};
document.Security ??= [];
document.Security.Add(securityRequirement);
return Task.CompletedTask;
}
}

View File

@@ -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<AppOptions>();
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<BearerOpenApiDocumentTransformer>();
options.AddSchemaTransformer<EnumOpenApiSchemaTransformer>();
});
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<JwtOptions>();
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;
}
}

View File

@@ -0,0 +1,49 @@
using System.ComponentModel;
using System.Text.Json.Nodes;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.OpenApi;
namespace StopShopping.AdminApi.Extensions;
/// <summary>
/// 处理enum类型openapi显示
/// </summary>
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<object>()
.Select(v => JsonNode.Parse(Convert.ToInt32(v).ToString())!)
.ToList();
schema.Enum = enumValues;
var enumNames = Enum.GetNames(context.JsonTypeInfo.Type);
schema.Extensions ??= new Dictionary<string, IOpenApiExtension>();
var namesExtension = new JsonNodeExtension(new JsonArray(
enumNames
.Select(n => (JsonNode)n)
.ToArray()));
schema.Extensions.Add("x-enumNames", namesExtension);
var descMap = new JsonObject();
foreach (var name in enumNames)
{
if (context.JsonTypeInfo.Type.GetField(name)
?.GetCustomAttributes(typeof(DescriptionAttribute), false)
.FirstOrDefault() is DescriptionAttribute attr)
{
descMap[name] = attr.Description;
}
}
schema.Extensions.Add("x-enumDescriptions", new JsonNodeExtension(descMap));
}
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,35 @@
using StopShopping.Services.Extensions;
namespace Microsoft.AspNetCore.Http;
public static class HttpExtensions
{
public const string REFRESH_TOKEN_COOKIE_KEY = "admin_refresh_token";
public static IResponseCookies AppendRefreshToken(
this IResponseCookies cookies,
IWebHostEnvironment env,
AppOptions appOptions,
TimeSpan maxAge,
string token)
{
CookieOptions options = new()
{
MaxAge = maxAge,
HttpOnly = true,
SameSite = SameSiteMode.Lax,
Domain = appOptions.CookieDomain,
};
if (!env.IsDevelopment())
options.Secure = true;
cookies.Append(
REFRESH_TOKEN_COOKIE_KEY,
token,
options);
return cookies;
}
}

View File

@@ -0,0 +1,13 @@
using StopShopping.AdminApi.Middlewares;
namespace Microsoft.AspNetCore.Builder;
public static class MiddlewareExtensions
{
public static IApplicationBuilder UseGlobalExceptionHandler(this IApplicationBuilder applicationBuilder)
{
applicationBuilder.UseMiddleware<GlobalExceptionHandlerMiddleware>();
return applicationBuilder;
}
}