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

.NET 8中HotChocolate GraphQL分布式查询限流问题求解

可以针对GraphQL查询实现细粒度限流,以下是适配HotChocolate + .NET 8分布式环境的具体方案

核心思路

AspNetCoreRateLimit仅能基于HTTP端点限流,要区分GraphQL下的不同查询,需要借助HotChocolate的请求管道能力,提取查询的操作名/字段名,结合Redis实现分布式计数限流。


方案一:全局中间件实现按操作名限流

通过自定义ASP.NET Core中间件,在GraphQL请求到达HotChocolate之前解析操作名,结合Redis针对「用户标识+操作名」的组合做计数限流。

1. 实现限流中间件

public class QueryRateLimitMiddleware
{
    private readonly RequestDelegate _next;
    private readonly IConnectionMultiplexer _redis;

    public QueryRateLimitMiddleware(RequestDelegate next, IConnectionMultiplexer redis)
    {
        _next = next;
        _redis = redis;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        // 解析GraphQL请求的操作名
        string? operationName = null;
        if (context.Request.Method == HttpMethods.Post)
        {
            var requestBody = await context.Request.ReadFromJsonAsync<GraphQLRequest>();
            operationName = requestBody?.OperationName ?? ExtractOperationName(requestBody?.Query);
        }
        else if (context.Request.Method == HttpMethods.Get)
        {
            operationName = context.Request.Query["operationName"].FirstOrDefault() ?? ExtractOperationName(context.Request.Query["query"].FirstOrDefault());
        }

        if (!string.IsNullOrEmpty(operationName))
        {
            // 获取用户标识(认证用户用ID,匿名用户用IP)
            var userKey = context.User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value 
                          ?? context.Connection.RemoteIpAddress?.ToString() ?? "anonymous";
            var rateLimitKey = $"graphql_rate_limit:{userKey}:{operationName}";
            
            var db = _redis.GetDatabase();
            var currentCount = await db.StringIncrementAsync(rateLimitKey);
            // 首次计数时设置过期时间(示例为1分钟)
            if (currentCount == 1)
            {
                await db.KeyExpireAsync(rateLimitKey, TimeSpan.FromMinutes(1));
            }

            // 限流阈值配置(可从配置文件读取)
            const int requestLimit = 10;
            if (currentCount > requestLimit)
            {
                context.Response.StatusCode = StatusCodes.Status429TooManyRequests;
                await context.Response.WriteAsJsonAsync(new 
                { 
                    message = $"操作「{operationName}」请求过于频繁,请1分钟后重试",
                    code = "RATE_LIMIT_EXCEEDED"
                });
                return;
            }
        }

        await _next(context);
    }

    // 从查询字符串中提取操作名(处理匿名查询场景)
    private string? ExtractOperationName(string? query)
    {
        if (string.IsNullOrWhiteSpace(query)) return "anonymous_query";
        var match = Regex.Match(query, @"(query|mutation)\s+(\w+)\s*{");
        return match.Success ? match.Groups[2].Value : "anonymous_query";
    }
}

// 匹配GraphQL请求结构的模型
public class GraphQLRequest
{
    public string? Query { get; set; }
    public string? OperationName { get; set; }
    public Dictionary<string, object>? Variables { get; set; }
}

2. 注册服务与中间件

var builder = WebApplication.CreateBuilder(args);

// 注册Redis连接(分布式环境必需)
builder.Services.AddSingleton<IConnectionMultiplexer>(sp =>
{
    var redisConfig = ConfigurationOptions.Parse(builder.Configuration.GetConnectionString("Redis"));
    return ConnectionMultiplexer.Connect(redisConfig);
});

// 注册HotChocolate
builder.Services.AddGraphQLServer()
    .AddQueryType<YourQueryType>();

var app = builder.Build();

// 必须在MapGraphQL之前注册限流中间件
app.UseMiddleware<QueryRateLimitMiddleware>();

app.MapGraphQL();
app.Run();

方案二:基于HotChocolate字段属性实现精准限流

如果需要针对单个查询/突变字段做独立限流,可以自定义属性结合HotChocolate的字段拦截能力实现。

1. 自定义限流属性

public class RateLimitAttribute : ObjectFieldDescriptorAttribute
{
    public int RequestLimit { get; }
    public int WindowSeconds { get; }

    public RateLimitAttribute(int requestLimit, int windowSeconds)
    {
        RequestLimit = requestLimit;
        WindowSeconds = windowSeconds;
    }

    protected override void OnConfigure(IDescriptorContext context, IObjectFieldDescriptor descriptor, MemberInfo member)
    {
        descriptor.Use(next => async ctx =>
        {
            var fieldName = ctx.Field.Name;
            var userKey = ctx.ContextData.TryGetValue("UserId", out var userId) ? userId.ToString() 
                          : ctx.Connection.RemoteIpAddress?.ToString() ?? "anonymous";
            var redis = ctx.Services.GetRequiredService<IConnectionMultiplexer>();
            var db = redis.GetDatabase();
            var rateLimitKey = $"graphql_rate_limit:{userKey}:{fieldName}";
            
            var currentCount = await db.StringIncrementAsync(rateLimitKey);
            if (currentCount == 1)
            {
                await db.KeyExpireAsync(rateLimitKey, TimeSpan.FromSeconds(WindowSeconds));
            }

            if (currentCount > RequestLimit)
            {
                throw new GraphQLException(new Error(
                    $"字段「{fieldName}」超过请求限制,请{WindowSeconds}秒后重试", 
                    "RATE_LIMIT_EXCEEDED"));
            }

            await next(ctx);
        });
    }
}

2. 在查询类型中使用属性

public class YourQueryType
{
    // 限制:每分钟最多10次请求
    [RateLimit(10, 60)]
    public User GetUser(int id)
    {
        // 业务逻辑
    }

    // 限制:每分钟最多5次请求
    [RateLimit(5, 60)]
    public List<Order> GetUserOrders(int userId)
    {
        // 业务逻辑
    }
}

分布式环境注意事项

  • 必须使用Redis作为共享计数存储,不能用内存存储,否则多实例间计数不统一。
  • 用户标识选择:认证用户优先用唯一ID(如JWT的sub声明),匿名用户可结合IP+UserAgent生成唯一标识,避免NAT环境下IP重复导致的误限流。
  • 批量查询处理:如果请求包含多个GraphQL操作,可根据需求选择按整个请求计数,或遍历每个操作分别计数。

内容的提问来源于stack exchange,提问作者Ashim upadhaya

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.17 12:34:51