前端是沒有任何需要登入的機制 也不需要驗證會員身份只是我想讓 API 安全些我如何在後端做個簡易的驗證確認前端來的請求都是合法的?而不能讓任何站點都可以請求這個 API(API的功能只有送前端的數據去
顺晟科技
2021-08-30 13:32:13
218
限流 :对某段时间内访问次数限制,保证系统的可用性和稳定性。防止突然访问暴增导致系统响应缓慢或者宕机。
  场景:在php-fpm中,fpm开启的子进程数是有限的,当并发请求大于可用子进程数时,进程池分配不了多余的子进程处理http请求,服务就会开始阻塞。导致nginx抛出502。
HttpLimitRequest模块该模块可以指定会话请求数量,可以通过指定ip进行请求频率限制。使用漏桶算法进行请求频率限制。
http { 
//会话状态存储在了10m的名称为"one"这个区域。该区域平均查询限制在每秒1个请求
  limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s; 
  
 ... server { ... location /search/ { 
 // 没秒平均请求不超过1个请求 突发不超过5个查询 如果不需要限制突发延迟内的超额请求,则应使用
 nodelay  limit_req zone=one burst= 5 nodelay;
 } 这是摘抄nginx文档中的一段关于限流的小例子。nginx使用的漏桶算法对用户访问频率进行限制。
通过百度、google 我们知道了。原来限流是基于算法来实现的。下面是限流的两种算法:
当然我们不仅要知其然,还要知其所以然。
漏桶算法:漏桶有一定的容量,且漏桶会漏水。 
当单位时间内注入的水大于单位时间内流出的水。漏桶积攒的水越来越多。直到溢出,如果溢出,则需要限流。
算法描述:
当前水量: 上次容量-流出容量+注入水量 
流出容量:(当前注水时间-上次注水时间)*流出速率
当 「当前水量」> 「桶子容量」 则溢出。否则正常,记录本次水量和注水时间。

新增BucketLimit.php类
  protected $capacity  = 60; //桶子总容量
  protected $addNum    = 20; //每次注入水的容量
  protected $rate      = 2;  //漏水速率
  protected $water_key = "water_capacity"; //缓存key
  public $redis;        //使用redis 缓存当前桶水量和上次注水时间
  public function __construct()
  {
        $redis = new \Redis();
        $this->redis= $redis;
        $this->redis->connect('127.0.0.1',6379);
  }
具体实现方法
 /**
     * @param $api [string 指定接口限流]
     * @param $addNum [int 注水量 ]
     * @return bool
     */
    public function bucket($addNum,$api='')
    {
        $this->addNum = $addNum;
        // 获取上次 桶内水量 注水时间
        list($waterCapacity,$waterTime,$lastTime) = $this->getLastWater();
        //计算出时间内流出的水量
        $lastWater = ($lastTime-$waterTime)*$this->rate;
        //本次水量
        $waterCapacity = $waterCapacity-$lastWater;
        //水量不能小于0
        $waterCapacity = ( $waterCapacity>=0 ) ? $waterCapacity : 0 ;
        $waterTime = $lastTime;
        //当前水量大于桶子容量 溢出返回 false 存储水量和注水时间
        if( ($waterCapacity+$addNum) <= $this->capacity ){
            $waterCapacity += $addNum;
            $this->setWater($waterCapacity,$waterTime);
            return true;
        }else{
           $this->setWater($waterCapacity,$waterTime);
            return false;
        }
  }
 
 /**
 * @return array [$waterCapacity,$waterTime,$lastTime] *  当前容量 上次漏水时间 当前时间
 */
 private function getLastWater()
{
    $water = $this->redis->get($this->water_key);
    if($water) {
        $water = json_decode($water,true);
        $waterCapacity =$water['water_capacity'];  //上一次容量
        $waterTime =$water['time']; //上一次注水时间
        $lastTime = time(); //本次注水时间
  } else{
        $this->redis->set($this->water_key,json_encode([
            'water_capacity'=>0,
            'time'=>time()
        ]));
        $waterCapacity =0;  //上一次容量
        $waterTime =time(); //上一次注水时间
        $lastTime = time(); //本次注水时间
  }
    return [$waterCapacity,$waterTime,$lastTime];
}
/**
 * @param $waterCapacity [int 本次剩余容量]
 * @param $waterTime [int 本次注水时间]
 */
 private function setWater($waterCapacity,$waterTime)
{
    $this->redis->set($this->water_key,json_encode([
        'water_capacity'=>$waterCapacity,
        'time'=>$waterTime
  ]));
}
  
  使用 for + sleep函数模拟请求 正常2s请求一次 方法正常不限流 小于2秒 请求到大概到第四次会进行限流
require_once 'BucketLimit.php';
$bucket = new BucketLimit();
for($i=1;$i<=100;$i++) {
 //根据for + sleep函数模拟请求 正常2s请求一次 方法正常不限流 sleep(1);
 $data =  $bucket->bucket(10);
  var_dump($data)."\n";
}令牌桶算法和漏桶算法刚好相反,指定速率向桶子里面投放令牌。每次请求都会想桶里面拿走一枚令牌,当桶子里面的令牌消费完毕,则限流。优点:可以方便改变投递令牌的速率。

hyperf 令牌桶算法实现限流代码
app/Http/Kernel.php    protected $middlewareGroups = [
           'api' => [
               'throttle:60,1', //执行中间件 每分钟请求限制在60次
           ],
       ]; protected $limiter;
    /**
     * Create a new request throttler.
     *
     * @param  \Illuminate\Cache\RateLimiter  $limiter
     * @return void
     */
    public function __construct(RateLimiter $limiter)
    {
        $this->limiter = $limiter;
    } /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @param  int|string  $maxAttempts
     * @param  float|int  $decayMinutes
     * @param  string  $prefix
     * @return \Symfony\Component\HttpFoundation\Response
     *
     * @throws \Illuminate\Http\Exceptions\ThrottleRequestsException
     */
    public function handle($request, Closure $next, $maxAttempts = 60, $decayMinutes = 1, $prefix = '')
    {
        //判断用户是否限制频率
        if (is_string($maxAttempts)
            && func_num_args() === 3
            && ! is_null($limiter = $this->limiter->limiter($maxAttempts))) {
            return $this->handleRequestUsingNamedLimiter($request, $next, $maxAttempts, $limiter);
        }
       //执行频率限制判断 参数分别是:
        return $this->handleRequest(
            $request, //请求类
            $next,    //中间件基类
            [
                (object) [
                    'key' => $prefix.$this->resolveRequestSignature($request), //缓存key
                    'maxAttempts' => $this->resolveMaxAttempts($request, $maxAttempts), //获取频繁阀值
                    'decayMinutes' => $decayMinutes,
                    'responseCallback' => null, //存放回调响应
                ],
            ]
        );
    }/**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @param  array  $limits
     * @return \Symfony\Component\HttpFoundation\Response
     *
     * @throws \Illuminate\Http\Exceptions\ThrottleRequestsException
     */
    protected function handleRequest($request, Closure $next, array $limits)
    {
        foreach ($limits as $limit) {
            //判断速率是否达到阀值 返回 true false 该方法使用缓存实例取出缓存的key
            if ($this->limiter->tooManyAttempts($limit->key, $limit->maxAttempts)) {
                throw $this->buildException($request, $limit->key, $limit->maxAttempts, $limit->responseCallback);
            }
            //类似于redis数值自增 并且设置过期时间
            $this->limiter->hit($limit->key, $limit->decayMinutes * 60);
        }
        $response = $next($request);
        //将响应放入响应回调函数中
        foreach ($limits as $limit) {
            $response = $this->addHeaders(
                $response,
                $limit->maxAttempts,
                $this->calculateRemainingAttempts($limit->key, $limit->maxAttempts)
            );
        }
        //返回响应
        return $response;
    }$this->limiter->tooManyAttempts方法
    /**
     * Determine if the given key has been "accessed" too many times.
     *
     * @param  string  $key
     * @param  int  $maxAttempts
     * @return bool
     */
    public function tooManyAttempts($key, $maxAttempts)
    {
        if ($this->attempts($key) >= $maxAttempts) {
            if ($this->cache->has($key.':timer')) {
                return true;
            }
            $this->resetAttempts($key);
        }
        return false;
    }该方法实现的原理:周期性限流。通过次数/时间来限制请求频率。
class CurrentLimiting
{
    protected $limit;
    protected $minutes;
    protected $redis;
    protected $key;
    /**
     * CurrentLimiting constructor.
     * @param string $api 接口
     * @param string $ip ip
     * @param int $limit 限制频率
     * @param int $minutes 分钟
     */
    public function __construct(string  $api,string $ip,int $limit,int $minutes)
    {
        $redis = new \Redis();
        $redis->connect('127.0.0.1','6379',3);
        $this->redis = $redis;
        $this->limit = $limit;
        $this->minutes = $minutes;
        $this->key = $ip.$api;
    }
    //获取请求次数
    public function attempts()
    {
      $count =  $this->redis->get($this->key);
      return is_null($count) ? 0 : $count;
    }
    /**
     *
     * @return bool
     */
    public function CurrentLimit()
    {
        $count = $this->attempts();
       if($count >= $this->limit) {
           return false;
       }
       if($count==0){
           $this->redis->set($this->key,0,$this->minutes*60);
       }
       //设置锁
       $this->redis->multi();
       $this->redis->watch();
       $this->redis->incr($this->key);
       return true;
    }
}15
2022-09
19
2021-09
30
2021-08