一步步实现对API的访问限制(节流)

一步步实现对API的访问限制(节流)

如果客户端很频繁的请求服务器,会给给服务器造成很大的压力,需要对客户端对API的请求,做一些限制,如Python 爬虫对服务器API的请求,对API的请求限制也是反爬虫的一个手段之一,那如何实现对API的访问的限制呢?

image

实现API接口

一个基本的API接口实现,没有任何的限制,客户端可以随意访问,也没有访问限制

[HttpGet]
[Route("~/api/helloworld")]
public HttpResponseMessage HelloWorld()
{
        return Request.CreateResponse(HttpStatusCode.OK, "Hello World");
}

添加基本的限制

如果要做限制,首先想到的是在访问这个接口时,做一个计数器,记录访问的数量,达到一定的数量之后就不能访问,使用cache来实现计数

[HttpGet]
[Route("~/api/helloworld")]
public HttpResponseMessage HelloWorld()
{
    int? requestCount = (int?)System.Web.HttpRuntime.Cache["throttle"];

    if (!requestCount.HasValue) requestCount = 0;

    requestCount++;

    HttpRuntime.Cache["throttle"] = requestCount;

    if (requestCount > 10) return Request.CreateResponse((HttpStatusCode)429, "Too many requests");

    return Request.CreateResponse(HttpStatusCode.OK, "Hello World");
}

这样如果访问 /api/helloworld 这个接口超过10次,就返回 429错误,但是这个实现是不能用于生产环境的,只能演示使用,虽然实现了访问限制,但是超过了次数之后,就无法访问这个接口了,这不是我们想要的,期望的是限制一段时间之后,用户可以重新访问这个API

添加过期时间

改造一下上面的代码,对访问的限制添加一个过期时间,如果超过了限制了,会在一段时间之后,就可以继续访问了

[HttpGet]
[Route("~/api/helloworld")]
public HttpResponseMessage HelloWorld()
{
    ThrottleInfo throttleInfo = (ThrottleInfo)HttpRuntime.Cache["throttle"];

    if (throttleInfo == null)
        throttleInfo = new ThrottleInfo {
                    ExpiresAt = DateTime.Now.AddSeconds(10), RequestCount = 0 };

    throttleInfo.RequestCount++;

    HttpRuntime.Cache.Add("throttle", throttleInfo, null,
        throttleInfo.ExpiresAt, Cache.NoSlidingExpiration,
                CacheItemPriority.Normal, null);

    if (throttleInfo.RequestCount > 10)
            return Request.CreateResponse((HttpStatusCode)429, "Too many requests");

    return Request.CreateResponse(HttpStatusCode.OK, "Hello World");
}

private class ThrottleInfo
{
    public DateTime ExpiresAt { get; set; }
    public int RequestCount { get; set; }
}

这样访问超过了限制,等一段时间,就可以继续访问了

封装一下代码,将访问限制的代码提取出来

public HttpResponseMessage HelloWorld()
{
    var throttler = new Throttler("helloworld");

    if (throttler.RequestShouldBeThrottled())
                return Request.CreateResponse(
                    (HttpStatusCode)429, "Too many requests");

    return Request.CreateResponse(HttpStatusCode.OK, "Hello World");
}
public class Throttler
{
    private int _requestLimit;
    private int _timeoutInSeconds;
    private string _key;

    public Throttler(string key, int requestLimit = 5, int timeoutInSeconds = 10)
    {
        _requestLimit = requestLimit;
        _timeoutInSeconds = timeoutInSeconds;
        _key = key;
    }

    public bool RequestShouldBeThrottled()
    {
        ThrottleInfo throttleInfo = (ThrottleInfo)HttpRuntime.Cache[_key];

        if (throttleInfo == null) throttleInfo = new ThrottleInfo {
                ExpiresAt = DateTime.Now.AddSeconds(_timeoutInSeconds),
                RequestCount = 0
        };

        throttleInfo.RequestCount++;

        HttpRuntime.Cache.Add(_key,
      throttleInfo,
      null,
      throttleInfo.ExpiresAt,
      Cache.NoSlidingExpiration,
      CacheItemPriority.Normal,
      null);

        return (throttleInfo.RequestCount > _requestLimit);
    }

    private class ThrottleInfo
    {
        public DateTime ExpiresAt { get; set; }
        public int RequestCount { get; set; }
    }
}

不一定需要依赖 HttpRuntime.Cache,使用 ConcurrentDictionary 实现cache

public class Throttler
{
    private int _requestLimit;
    private int _timeoutInSeconds;
    private string _key;
    private static ConcurrentDictionary<string, ThrottleInfo> _cache =
        new ConcurrentDictionary<string, ThrottleInfo>();

    public Throttler(string key, int requestLimit = 5, int timeoutInSeconds = 10)
    {
        _requestLimit = requestLimit;
        _timeoutInSeconds = timeoutInSeconds;
        _key = key;
    }

    public bool RequestShouldBeThrottled()
    {
        ThrottleInfo throttleInfo = _cache.ContainsKey(_key) ? _cache[_key] : null;

        if (throttleInfo == null || throttleInfo.ExpiresAt <= DateTime.Now)
        {
            throttleInfo = new ThrottleInfo {
                ExpiresAt = DateTime.Now.AddSeconds(_timeoutInSeconds),
                RequestCount = 0};
        };

        throttleInfo.RequestCount++;

        _cache[_key] = throttleInfo;

        return (throttleInfo.RequestCount > _requestLimit);
    }

    private class ThrottleInfo
    {
        public DateTime ExpiresAt { get; set; }
        public int RequestCount { get; set; }
    }
}

使用user id作为key,也可以使用IP地址作为key

  • UserName作为key
[HttpGet]
[Route("~/api/helloworld")]
public HttpResponseMessage HelloWorld()
{
    var throttler = new Throttler(User.Identity.Name);
}
  • 使用IP地址作为key
[HttpGet]
[Route("~/api/helloworld")]
public HttpResponseMessage HelloWorld()
{
    var ipAddress = System.Web.HttpContext.Current.Request.ServerVariables["REMOTE_ADDR"];
    var throttler = new Throttler(ipAddress);
}

让客户端知道访问限制,已经过期时间等

封装一下返回结果

public HttpResponseMessage HelloWorld()
{
    var throttler = new Throttler(User.Identity.Name);

    HttpResponseMessage response = createResponse("Hello World", throttler);

    return response;
}

private HttpResponseMessage createResponse(object content, Throttler throttler)
{
    HttpResponseMessage response;

    if (throttler.RequestShouldBeThrottled())
        response = Request.CreateResponse((HttpStatusCode)429, "Too many requests");
    else
        response = Request.CreateResponse(HttpStatusCode.OK, content);

    response.Headers.Add("X-RateLimit-Limit", throttler.RequestLimit.ToString());
    response.Headers.Add("X-RateLimit-Remaining", throttler.RequestsRemaining.ToString());
    response.Headers.Add("X-RateLimit-Reset", toUnixTime(throttler.WindowResetDate).ToString());

    return response;
}

private long toUnixTime(DateTime date)
{
    var epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
    return Convert.ToInt64((date.ToUniversalTime() - epoch).TotalSeconds);
}

在返回的结果中添加了三个 header ,重构一下 Throttler来支持这个三个属性

public class Throttler
{
    public int RequestLimit { get; private set; }
    public int RequestsRemaining { get; private set; }
    public DateTime WindowResetDate { get; private set; }
    private static ConcurrentDictionary<string, ThrottleInfo> _cache =
        new ConcurrentDictionary<string, ThrottleInfo>();

    private string _key;
    private int _timeoutInSeconds;

    public Throttler(string key, int requestLimit = 5, int timeoutInSeconds = 10)
    {
        RequestLimit = requestLimit;
        _timeoutInSeconds = timeoutInSeconds;
        _key = key;
    }

    public bool RequestShouldBeThrottled()
    {
        ThrottleInfo throttleInfo = _cache.ContainsKey(_key) ? _cache[_key] : null;

        if (throttleInfo == null || throttleInfo.ExpiresAt <= DateTime.Now)
        {
            throttleInfo = new ThrottleInfo {
                ExpiresAt = DateTime.Now.AddSeconds(_timeoutInSeconds),
                RequestCount = 0};
        };

        WindowResetDate = throttleInfo.ExpiresAt;

        throttleInfo.RequestCount++;

        _cache[ThrottleGroup] = throttleInfo;

        RequestsRemaining = Math.Max(RequestLimit - throttleInfo.RequestCount, 0);

        return (throttleInfo.RequestCount > RequestLimit);
    }

    private class ThrottleInfo
    {
        public DateTime ExpiresAt { get; set; }
        public int RequestCount { get; set; }
    }
}

创建一个属性

using System;
using System.Net;
using System.Net.Http;
using System.Runtime.CompilerServices;
using System.Web;
using System.Web.Http.Controllers;
using System.Web.Http.Filters;

namespace Throttling
{
    public class ThrottleFilter : ActionFilterAttribute
    {
        private Throttler _throttler;
        private string _throttleGroup;

        public ThrottleFilter(
            int RequestLimit = 5,
            int TimeoutInSeconds = 10,
            [CallerMemberName] string ThrottleGroup = null)
        {
            _throttleGroup = ThrottleGroup;
            _throttler = new Throttler(ThrottleGroup, RequestLimit, TimeoutInSeconds);
        }

        public override void OnActionExecuting(HttpActionContext actionContext)
        {
            setIdentityAsThrottleGroup();

            if (_throttler.RequestShouldBeThrottled)
            {
                actionContext.Response = actionContext.Request.CreateResponse(
                    (HttpStatusCode)429, "Too many requests");

                addThrottleHeaders(actionContext.Response);
            }

            base.OnActionExecuting(actionContext);
        }

        public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
        {
            setIdentityAsThrottleGroup();
            if (actionExecutedContext.Exception == null) _throttler.IncrementRequestCount();
            addThrottleHeaders(actionExecutedContext.Response);
            base.OnActionExecuted(actionExecutedContext);
        }

        private void setIdentityAsThrottleGroup()
        {
            if (_throttleGroup == "identity")
                _throttler.ThrottleGroup = HttpContext.Current.User.Identity.Name;

            if (_throttleGroup == "ipaddress")
                _throttler.ThrottleGroup = HttpContext.Current.Request.UserHostAddress;
        }

        private void addThrottleHeaders(HttpResponseMessage response)
        {
            if (response == null) return;

            foreach (var header in _throttler.GetRateLimitHeaders())
                response.Headers.Add(header.Key, header.Value);
        }
    }
}

对应的 Throttler代码

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;

namespace Throttling
{
    public class Throttler
    {
        public int RequestLimit { get; private set; }
        public int RequestsRemaining { get; private set; }
        public DateTime WindowResetDate { get; private set; }
        private static ConcurrentDictionary<string, ThrottleInfo> _cache =
            new ConcurrentDictionary<string, ThrottleInfo>();

        public string ThrottleGroup { get; set; }
        private int _timeoutInSeconds;

        public Throttler(string key, int requestLimit = 5, int timeoutInSeconds = 10)
        {
            RequestLimit = requestLimit;
            _timeoutInSeconds = timeoutInSeconds;
            ThrottleGroup = key;
        }

        private ThrottleInfo getThrottleInfoFromCache()
        {
            ThrottleInfo throttleInfo =
                _cache.ContainsKey(ThrottleGroup) ? _cache[ThrottleGroup] : null;

            if (throttleInfo == null || throttleInfo.ExpiresAt <= DateTime.Now)
            {
                throttleInfo = new ThrottleInfo
                {
                    ExpiresAt = DateTime.Now.AddSeconds(_timeoutInSeconds),
                    RequestCount = 0
                };
            };

            return throttleInfo;
        }

        public bool RequestShouldBeThrottled
        {
            get
            {
                ThrottleInfo throttleInfo = getThrottleInfoFromCache();
                WindowResetDate = throttleInfo.ExpiresAt;
                RequestsRemaining = Math.Max(RequestLimit - throttleInfo.RequestCount, 0);
                return (throttleInfo.RequestCount > RequestLimit);
            }
        }

        public void IncrementRequestCount()
        {
            ThrottleInfo throttleInfo = getThrottleInfoFromCache();
            throttleInfo.RequestCount++;
            _cache[ThrottleGroup] = throttleInfo;
        }

        private class ThrottleInfo
        {
            public DateTime ExpiresAt { get; set; }
            public int RequestCount { get; set; }
        }

        public Dictionary<string,string> GetRateLimitHeaders()
        {
            ThrottleInfo throttleInfo = getThrottleInfoFromCache();

            int requestsRemaining = Math.Max(RequestLimit - throttleInfo.RequestCount, 0);

            var headers = new Dictionary<string,string>();
            headers.Add("X-RateLimit-Limit", RequestLimit.ToString());
            headers.Add("X-RateLimit-Remaining", RequestsRemaining.ToString());
            headers.Add("X-RateLimit-Reset", toUnixTime(throttleInfo.ExpiresAt).ToString());
            return headers;
        }

        private long toUnixTime(DateTime date)
        {
            var epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
            return Convert.ToInt64((date.ToUniversalTime() - epoch).TotalSeconds);
        }

    }
}

如何使用

  • 基本使用
[ThrottleFilter()]
[HttpGet]
[Route("~/api/helloworld")]
public HttpResponseMessage HelloWorld()
{
  return Request.CreateResponse(HttpStatusCode.OK, "Hello World");
}
  • 添加限制
[ThrottleFilter(RequestLimit: 50, TimeoutInSeconds: 5)]
[HttpGet]
[Route("~/api/allow-more")]
public HttpResponseMessage HelloWorld2()
{
    return Request.CreateResponse(HttpStatusCode.OK, "Hello World2");
}
  • 使用IP地址
[ThrottleFilter(ThrottleGroup: "ipaddress")]
[HttpGet]
[Route("~/api/name")]
public HttpResponseMessage GetName(int id)
{
    return Request.CreateResponse(HttpStatusCode.OK, "John Smith");
}
  • 使用user Id
[ThrottleFilter(ThrottleGroup: "identity")]
[HttpGet]
[Route("~/api/name")]
public HttpResponseMessage GetName(int id)
{
    return Request.CreateResponse(HttpStatusCode.OK, "Jane Doe");
}

也可以使用第三方库实现节流 WebApiThrottle Github

博客

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,236评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,867评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,715评论 0 340
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,899评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,895评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,733评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,085评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,722评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,025评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,696评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,816评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,447评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,057评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,254评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,204评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,561评论 2 343

推荐阅读更多精彩内容