求助:使用IDocumentFilter结合JWT Claims过滤Swagger端点时Claims为空
问题:使用IDocumentFilter基于JWT Claims过滤Swagger端点时Claims为空
我尝试通过IDocumentFilter根据JWT令牌中的用户Claims过滤Swagger端点,但在IDocumentFilter的实现代码被调用时,HttpContext里的用户Claims始终是空的;但调用其他业务接口时,用户Claims是正常可用的。
启动代码
public static async void Launch(string[] args) { var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); string[] swaggerEndpoints = new string[] { "Test", "test", }; builder.Services.AddSwaggerGen(o => { for (int i = 0; i < swaggerEndpoints.Length; i += 2) { o.SwaggerDoc(swaggerEndpoints[i], new OpenApiInfo() { Version = "v1", Title = swaggerEndpoints[i], Description = swaggerEndpoints[i + 1] }); } o.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme() { Name = "Authorization", Type = SecuritySchemeType.ApiKey, Scheme = "Bearer", BearerFormat = "JWT", In = ParameterLocation.Header, Description = "JSON Web Token based security", }); o.AddSecurityRequirement(new OpenApiSecurityRequirement() { { new OpenApiSecurityScheme { Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "Bearer" } }, new string[] {} } }); o.DocumentFilter<CheckAuthedUserFilter>(); }); builder.Services.AddHttpContextAccessor(); // Add JWT configuration builder.Services.AddAuthentication(o => { o.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; o.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; o.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; }).AddJwtBearer(o => { o.TokenValidationParameters = new TokenValidationParameters { ValidIssuer = builder.Configuration["Jwt:Issuer"], ValidAudience = builder.Configuration["Jwt:Audience"], IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"])), ValidateIssuer = true, ValidateAudience = true, ValidateLifetime = true, ValidateIssuerSigningKey = true, ClockSkew = System.TimeSpan.Zero }; }); builder.Services.AddAuthorization(); builder.Services.AddEndpointsApiExplorer(); //security certificates if (File.Exists(builder.Configuration["SSL:certLocation"])) { builder.WebHost.ConfigureKestrel(serverOptions => { var appServices = serverOptions.ApplicationServices; serverOptions.ListenAnyIP(8080, portOptions => { portOptions.UseHttps(builder.Configuration["SSL:certLocation"], builder.Configuration["SSL:certPassword"], h => { }); }); }); } app = builder.Build(); app.UseHttpsRedirection(); app.UseCors("CorsList"); app.UseAuthentication(); app.UseAuthorization(); app.UseRouting(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); app.UseSwagger(); app.UseSwaggerUI(c => { for (int i = 0; i < swaggerEndpoints.Length; i += 2) { c.SwaggerEndpoint($"/swagger/{swaggerEndpoints[i]}/swagger.json", swaggerEndpoints[i]); } c.ConfigObject.AdditionalItems.Add("persistAuthorization", true); c.DefaultModelExpandDepth(-1); }); app.Start(); }
IDocumentFilter实现代码
public class CheckAuthedUserFilter : IDocumentFilter { private readonly IHttpContextAccessor httpContextAccessor; public AddAuthHeaderOperationFilter(IHttpContextAccessor httpContextAccessor) { this.httpContextAccessor = httpContextAccessor; } public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context) { //httpContextAccessor.HttpContext.User.Claims is always empty and the identity is not authenticated foreach (var claim in httpContextAccessor.HttpContext.User.Claims) { Console.WriteLine(claim.Type + " " + claim.Value); } } }
我查过资料,发现IDocumentFilter似乎在认证/授权流程完成前就被调用了。很多建议说把app.UseAuthentication()、app.UseAuthorization()和app.UseRouting()放在app.UseSwagger()和app.UseSwaggerUI(...)之前,但这么做也没解决我的问题。
解决方案
核心原因
IDocumentFilter是在Swagger文档首次生成时(应用启动阶段)执行的,而非每次请求Swagger文档时。此时没有HTTP请求上下文,自然拿不到当前用户的Claims——调整中间件顺序无法解决这个本质问题。
方案1:使用IOperationFilter(推荐)
IOperationFilter会在每次请求Swagger文档时执行,能获取当前请求的HttpContext:
- 实现
IOperationFilter:
public class CheckAuthedUserOperationFilter : IOperationFilter { private readonly IHttpContextAccessor _httpContextAccessor; public CheckAuthedUserOperationFilter(IHttpContextAccessor httpContextAccessor) { _httpContextAccessor = httpContextAccessor; } public void Apply(OpenApiOperation operation, OperationFilterContext context) { var user = _httpContextAccessor.HttpContext.User; if (!user.Identity.IsAuthenticated) { // 未认证时标记接口为隐藏 operation.Extensions.Add("x-hidden", true); return; } // 根据用户Claims过滤接口(示例:检查接口所需权限) var requiredPermission = context.MethodInfo.GetCustomAttribute<AuthorizeAttribute>()?.Policy; if (!string.IsNullOrEmpty(requiredPermission) && !user.HasClaim("Permission", requiredPermission)) { operation.Extensions.Add("x-hidden", true); } } }
- 在Swagger配置中注册该Filter:
builder.Services.AddSwaggerGen(o => { // ... 原有配置 o.OperationFilter<CheckAuthedUserOperationFilter>(); // 替换原DocumentFilter });
- 添加自定义脚本让Swagger UI隐藏标记的接口:
- 在
wwwroot/swagger-ui目录下创建custom.js:
- 在
window.addEventListener('load', function() { const hiddenOperations = document.querySelectorAll('[data-operation-id]'); hiddenOperations.forEach(op => { const operationId = op.getAttribute('data-operation-id'); const spec = window.ui.getSpec(); Object.values(spec.paths).forEach(pathItem => { Object.values(pathItem).forEach(operation => { if (operation.operationId === operationId && operation['x-hidden']) { op.style.display = 'none'; } }); }); }); });
- 在SwaggerUI配置中注入脚本:
app.UseSwaggerUI(c => { // ... 原有配置 c.InjectJavascript("/swagger-ui/custom.js"); });
方案2:动态生成Swagger文档(彻底移除无权限路径)
通过自定义Swagger端点,每次请求时重新生成并过滤文档:
- 移除原
app.UseSwagger(),添加自定义端点:
app.MapGet("/swagger/{documentName}/swagger.json", async (string documentName, HttpContext context, ISwaggerProvider swaggerProvider) => { var swaggerDoc = await swaggerProvider.GetSwaggerAsync(documentName); var user = context.User; if (!user.Identity.IsAuthenticated) { // 移除所有需要授权的路径 var pathsToRemove = swaggerDoc.Paths .Where(p => p.Value.Operations.Any(o => o.Value.Security.Any())) .Select(p => p.Key) .ToList(); foreach (var path in pathsToRemove) { swaggerDoc.Paths.Remove(path); } return Results.Json(swaggerDoc); } // 根据用户Claims过滤路径(示例:匹配权限Claim) var userPermissions = user.Claims .Where(c => c.Type == "Permission") .Select(c => c.Value) .ToList(); var pathsToRemove = new List<string>(); foreach (var (path, pathItem) in swaggerDoc.Paths) { foreach (var (_, operation) in pathItem.Operations) { var requiredPolicy = operation.Extensions.TryGetValue("x-policy", out var obj) ? obj.ToString() : null; if (!string.IsNullOrEmpty(requiredPolicy) && !userPermissions.Contains(requiredPolicy)) { pathsToRemove.Add(path); break; } } } foreach (var path in pathsToRemove) { swaggerDoc.Paths.Remove(path); } return Results.Json(swaggerDoc); }).RequireAuthorization();
- 保留原SwaggerUI配置即可,它会自动指向这个自定义端点。
额外注意事项
- 你的
CheckAuthedUserFilter构造函数名称错误,应与类名一致:public CheckAuthedUserFilter(IHttpContextAccessor httpContextAccessor),否则依赖注入会失败。 - 调整中间件顺序为正确的执行流程:
app.UseHttpsRedirection(); app.UseCors("CorsList"); app.UseRouting(); app.UseAuthentication(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); // 方案1保留,方案2移除 app.UseSwagger(); app.UseSwaggerUI(c => { // ... 配置 });
内容的提问来源于stack exchange,提问作者jordan t
相关产品推荐
相关产品推荐

