You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

求助:使用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:

  1. 实现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);
        }
    }
}
  1. 在Swagger配置中注册该Filter:
builder.Services.AddSwaggerGen(o =>
{
    // ... 原有配置
    o.OperationFilter<CheckAuthedUserOperationFilter>(); // 替换原DocumentFilter
});
  1. 添加自定义脚本让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端点,每次请求时重新生成并过滤文档:

  1. 移除原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();
  1. 保留原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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.09 17:47:04