如何限制每个客户端的WCF服务

时间:2015-02-11 04:42:34

标签: c# wcf

我正在开发一项服务,该服务将在互联网上暴露给少数精选客户。但是,我不希望一个客户端能够经常调用该服务,以致它们阻止另一个客户端调用该服务,或者在合理的时间内响应。我意识到WCF内置了许多限制配置设置,但据我所知,这些仅适用于整个服务。

是否有任何内置机制可以让我配置服务,使单个客户端只能执行(例如)10个并发呼叫或类似的呼叫?

这个问题与另一个问题有关:

Best way to secure a WCF service on the internet with few clients

我还在努力确定是否需要,以及如何识别个人客户。

3 个答案:

答案 0 :(得分:2)

您在这里寻找的短语是 Rate limiting 。而且,不,没有内置的方法来限制WCF服务。如您所说,您可以使用service throttling周围的WCF功能集,但这是服务级别设置,而不是每个客户端。

为了实现速率限制,一般指导似乎是使用内存中的集合(或类似于redis用于横向扩展方案)来对传入的用户字符串或IP地址执行快速查找。然后,您可以围绕该信息定义一些限制算法。

更多信息herehere

答案 1 :(得分:1)

首先,如果您使用某种负载均衡器,最好在那里实施。例如,NGINX具有速率限制功能:http://nginx.org/en/docs/http/ngx_http_limit_req_module.html

其次,您应该考虑使用称为动态IP限制的内置IIS速率限制功能:http://www.iis.net/learn/get-started/whats-new-in-iis-8/iis-80-dynamic-ip-address-restrictions

如果这两者都不够,因为您需要自定义逻辑,您可以始终在应用程序级别实现它。这可以通过多种方式完成。

让我们从一些可重复使用的速率限制逻辑开始:

public interface IRateLimiter
{
    bool ShouldLimit(string key);

    HttpStatusCode LimitStatusCode { get; }
}

public interface IRateLimiterConfiguration
{
    int Treshhold { get; set; }
    TimeSpan TimePeriod { get; set; }
    HttpStatusCode LimitStatusCode { get; set; }
}

public class RateLimiterConfiguration : System.Configuration.ConfigurationSection, IRateLimiterConfiguration
{
    private const string TimePeriodConst = "timePeriod";
    private const string LimitStatusCodeConst = "limitStatusCode";
    private const string TreshholdConst = "treshhold";
    private const string RateLimiterTypeConst = "rateLimiterType";

    [ConfigurationProperty(TreshholdConst, IsRequired = true, DefaultValue = 10)]
    public int Treshhold
    {
        get { return (int)this[TreshholdConst]; }
        set { this[TreshholdConst] = value; }
    }

    [ConfigurationProperty(TimePeriodConst, IsRequired = true)]
    [TypeConverter(typeof(TimeSpanConverter))]
    public TimeSpan TimePeriod
    {
        get { return (TimeSpan)this[TimePeriodConst]; }
        set { this[TimePeriodConst] = value; }
    }

    [ConfigurationProperty(LimitStatusCodeConst, IsRequired = false, DefaultValue = HttpStatusCode.Forbidden)]
    public HttpStatusCode LimitStatusCode
    {
        get { return (HttpStatusCode)this[LimitStatusCodeConst]; }
        set { this[LimitStatusCodeConst] = value; }
    }

    [ConfigurationProperty(RateLimiterTypeConst, IsRequired = true)]
    [TypeConverter(typeof(TypeNameConverter))]
    public Type RateLimiterType
    {
        get { return (Type)this[RateLimiterTypeConst]; }
        set { this[RateLimiterTypeConst] = value; }
    }
}

public class RateLimiter : IRateLimiter
{
    private readonly IRateLimiterConfiguration _configuration;        
    private static readonly MemoryCache MemoryCache = MemoryCache.Default;

    public RateLimiter(IRateLimiterConfiguration configuration)
    {
        _configuration = configuration;
    }

    public virtual bool ShouldLimit(string key)
    {
        if (!string.IsNullOrEmpty(key))
        {
            Counter counter = new Counter {Count = 1};
            counter = MemoryCache.AddOrGetExisting(key, new Counter { Count = 1 }, DateTimeOffset.Now.Add(_configuration.TimePeriod)) as Counter ?? counter;
            lock (counter.LockObject)
            {
                if (counter.Count < _configuration.Treshhold)
                {
                    counter.Count++;
                }
                else
                {
                    return true;
                }
            }
        }

        return false;
    }

    public HttpStatusCode LimitStatusCode
    {
        get { return _configuration.LimitStatusCode; }
    }

    private class Counter
    {
        public volatile int Count;
        public readonly object LockObject = new object();
    }
}

public class RateLimiterFactory
{
    public IRateLimiter CreateRateLimiter()
    {
        var configuration = GetConfiguration();
        return (IRateLimiter)Activator.CreateInstance(configuration.RateLimiterType, configuration);
    }

    public static RateLimiterConfiguration GetConfiguration()
    {
        return ConfigurationManager.GetSection("rateLimiter") as RateLimiterConfiguration ?? new RateLimiterConfiguration();
    }
}

static class GetClientIpExtensions
{
    private const string XForwardedForHeaderName = "X-Forwarded-For";
    private const string HttpXForwardedForServerVariableName = "HTTP_X_FORWARDED_FOR";
    private const string HttpRemoteAddressServerVariableName = "REMOTE_ADDR";

    public static string GetClientIp(this Message message)
    {
        return GetClientIp(message.Properties);
    }

    public static string GetClientIp(this OperationContext context)
    {
        return GetClientIp(context.IncomingMessageProperties);
    }

    public static string GetClientIp(this MessageProperties messageProperties)
    {
        var endpointLoadBalancer = messageProperties[HttpRequestMessageProperty.Name] as HttpRequestMessageProperty;
        if (endpointLoadBalancer != null && endpointLoadBalancer.Headers[XForwardedForHeaderName] != null)
        {
            return endpointLoadBalancer.Headers[XForwardedForHeaderName];
        }
        else
        {
            var endpointProperty = messageProperties[RemoteEndpointMessageProperty.Name] as RemoteEndpointMessageProperty;
            return (endpointProperty == null) ? string.Empty : endpointProperty.Address;
        }
    }

    public static string GetClientIp(this HttpRequest request)
    {
        string ipList = request.ServerVariables[HttpXForwardedForServerVariableName];
        return !string.IsNullOrEmpty(ipList) ? ipList.Split(',')[0] : request.ServerVariables[HttpRemoteAddressServerVariableName];
    }
}

这使用配置,使用接口和默认MemoryCache进行适当的隔离。您可以轻松更改实现以抽象缓存。这将允许使用不同的缓存提供程序,例如redis。如果您希望为运行相同服务的多个服务器提供分布式缓存,这可能很有用。

现在以此代码为基础,我们可以使用它添加一些实现。我们可以添加一个IHttpModule:

public class RateLimiterHttpModule : IHttpModule
{
    private readonly IRateLimiter _rateLimiter;

    public RateLimiterHttpModule()
    {
        _rateLimiter = new RateLimiterFactory().CreateRateLimiter();
    }

    public void Init(HttpApplication context)
    {
        context.BeginRequest += OnBeginRequest;
    }

    private void OnBeginRequest(object sender, EventArgs e)
    {
        HttpApplication application = (HttpApplication)sender;
        string ip = application.Context.Request.GetClientIp();
        if (_rateLimiter.ShouldLimit(ip))
        {
            TerminateRequest(application.Context.Response);
        }
    }

    private void TerminateRequest(HttpResponse httpResponse)
    {
        httpResponse.StatusCode = (int)_rateLimiter.LimitStatusCode;
        httpResponse.SuppressContent = true;
        httpResponse.End();
    }

    public void Dispose()
    {
    }
}

或者仅适用于任何传输级别的WCF实现:

public class RateLimiterDispatchMessageInspector : IDispatchMessageInspector
{
    private readonly IRateLimiter _rateLimiter;

    public RateLimiterDispatchMessageInspector(IRateLimiter rateLimiter)
    {
        _rateLimiter = rateLimiter;
    }

    public object AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext)
    {
        if (_rateLimiter.ShouldLimit(request.GetClientIp()))
        {
            request = null;
            return _rateLimiter.LimitStatusCode;
        }
        return null;
    }

    public void BeforeSendReply(ref Message reply, object correlationState)
    {
        if (correlationState is HttpStatusCode)
        {
            HttpResponseMessageProperty responseProperty = new HttpResponseMessageProperty();
            reply.Properties["httpResponse"] = responseProperty;
            responseProperty.StatusCode = (HttpStatusCode)correlationState;
        }
    }
}

public class RateLimiterServiceBehavior : IServiceBehavior
{
    public void Validate(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase) { }

    public void AddBindingParameters(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase, Collection<ServiceEndpoint> endpoints, BindingParameterCollection bindingParameters) { }

    public void ApplyDispatchBehavior(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
    {
        var rateLimiterFactory = new RateLimiterFactory();

        foreach (ChannelDispatcher chDisp in serviceHostBase.ChannelDispatchers)
        {
            foreach (EndpointDispatcher epDisp in chDisp.Endpoints)
            {
                epDisp.DispatchRuntime.MessageInspectors.Add(new RateLimiterDispatchMessageInspector(rateLimiterFactory.CreateRateLimiter()));
            }
        }
    }
}

public class RateLimiterBehaviorExtensionElement : BehaviorExtensionElement
{
    protected override object CreateBehavior()
    {
        return new RateLimiterServiceBehavior();
    }

    public override Type BehaviorType
    {
        get { return typeof(RateLimiterServiceBehavior); }
    }
}

您可以类似地为ASP.NET MCV执行操作过滤器。请在此处查看:How do I implement rate limiting in an ASP.NET MVC site?

答案 2 :(得分:0)

您可以改变您的配置:

 ConcurrencyMode:=ConcurrencyMode.Single
 InstanceContextMode:=InstanceContextMode.Single

然后,在代码中,设置两个服务级变量:

  • 一个字符串变量,用于保存最后一个请求者的ID
  • 访问次数的一个整数变量。

对于每个请求,其中入站用户的ID ==最后保存的用户,请将整数变量增加+1。在请求10之后,向用户返回拒绝。如果用户不同,则重置变量并处理请求。

这不是配置解决方案 - 它的配置和代码,但它可以工作。