通过每个用户的总请求限制API端点

时间:2019-01-18 07:01:29

标签: node.js express

我目前正在探索解决方案,以按每月请求总数限制对NodeJS上的API端点的访问。

例如,我希望免费计划用户访问/api端点,每个月最多可以访问100个请求,高级计划用户每月可以访问5000个请求。

解决这个问题的天真的方法是实施护照中间件来获取用户的计划,然后跟踪计数:

  app.get("/api", requireAuth, async (req, res, next) => {
    try {
        // Check if user ran out of requests
        if (req.user.apiRequestsLeft === 0) {
          res.send("You ran out of API requests!")
        } else {
          // Decrement the allocated requests
          req.user.apiRequestsLeft--;
          await req.user.save();
          res.send(user)
        }
    } catch (err) {
      next(err);
    }
  });

我担心的是:

  1. 每次有请求时都必须更新MongoDB文档的性能/可伸缩性问题-这是否可行?或者在应用增长时会遇到问题吗?
  2. 重置计数-如果这是每天的cronjob,以查看每个用户的“注册”时间戳,计算是否已过去一个月并相应地重置分配的请求,或者有更好的方法来设计类似这个吗?

2 个答案:

答案 0 :(得分:2)

  

必须更新MongoDB文档的性能/可伸缩性问题   每次有请求时-这可行还是我遇到问题   应用程序何时增长?

当然。您很快就会遇到大量的mongoDB流量,它将遇到性能瓶颈。我认为,您应该使用速度更快的内存数据库,例如Redis来处理这种情况。您甚至可以将Redis用作session-store,这将减少MongoDB上的负载。这样,MongoDB可以用于其他业务查询。

  

重置计数-如果这是每天的cronjob,   每个用户的“注册”时间戳,计算一个月   已经通过并相应地重置了分配的请求,或者是否存在   设计这样的更好的方法?

更好的方法是在中间件本身中实现重置部分。

这是一些解释我解决方案的代码。

Quota对象的示例设计为:

{
    type: "FREE_USER",                  /** or "PREMIUM_USER" */
    access_limit: 100,                  /** or 5000 */
    exhausted_requests: 42              /** How many requests the user has made so far this month */
    last_reset_timestamp: 1547796508728 /** When was the exhausted_requests set to 0 last time */
}

采用这种设计。您的检查配额的中间件看起来像:

const checkQuota = async (req, res, next) => {
    const user = req.user;
    const userQuotaStr = await redis.getAsync(user.id)
    let userQuota;
    /** Check if we have quota information about user */
    if (userQuotaStr != null) {
        /** We have previously saved quota information */
        userQuota = JSON.parse(userQuotaStr);

        /** 
         * Check if we should reset the exhausted_requests
         * Assuming that all the requests are reset on the First Day of each month.
         */
        if ( isStartOfMonth() ) {
            /** 
             * It is First Day of the month. We might need to reset the `exhausted_requests` 
             * Check the difference between `Date.now()` and `userQuota.last_reset_timestamp`
             * to determine whether we should reset or not
             */
            if ( shouldResetTimeStamp(userQuota.last_reset_timestamp) ) {
                userQuota.exhausted_requests = 0
                userQuota.last_reset_timestamp = Date.now()
            }
        }
    } else {
        /** We do not have previously saved quota information. Prepare one */
        userQuota = {
            type: user.type,
            access_limit: user.access_limit,
            exhausted_requests: 0,
            last_reset_timestamp: Date.now()
        }
    }

    /** Incredement the counter to account the current request */
    userQuota.exhausted_requests++

    /** Update in database */
    redis.set(user.id, JSON.stringify(userQuota))


    if ( userQuota.exhausted_requests >= userQuota.access_limit ) {
        /** User has reached the quota limit. Deny the request. set with 401 or 403 status code */
    } else {
        /** User can access the API. call next() */
    }   
}

当然,该代码段是不完整的。它只是为您提供有关编写中间件的想法。

以下是您可以将中间件用于API的方式:

/** If requests to routes are under the quota */
app.get("/api/quota-routes", requireAuth, checkQuota, /** Mount the actual middleware here */)

/** If requests to routes are unlimited, just remove the checkQuota middleware */
app.get("/api/unlimited-routes", requireAuth, /** Mount the actual middleware here */)

答案 1 :(得分:0)

rate-limiter-flexible软件包可帮助您创建计数器并自动使计数器过期。

const opts = {
  storeClient: mongoConn,
  points: 5000, // Number of points
  duration: 60 * 60 * 24 * 30, // Per month
};
const rateLimiterMongo = new RateLimiterMongo(opts);
const rateLimiterMiddleware = (req, res, next) => {
   // req.userId should be set before this middleware
   const key = req.userId ? req.userId : req.ip;
   const pointsToConsume = req.userId ? 1 : 50;
   rateLimiterMongo.consume(key, pointsToConsume)
      .then(() => {
          next();
      })
      .catch(_ => {
          res.status(429).send('Too Many Requests');
      });
   };

app.use(rateLimiterMiddleware);

请注意,此示例未绑定到日历月,而是计算了下一个月内第一个事件的事件。您可以使用 block严格将计数器的到期日与日历月份联系起来。

在基本服务器上,此代码应该可以轻松地每秒处理大约1k-2k请求。您也可以使用Redis limiterMongo limiter with sharding options

此外,它提供In-memory Block strategy以避免对MongoDB / Redis /任何商店的请求过多。

或者,使用get中的rate-limiter-flexible方法来减少不必要的计数器更新量。 get方法比增量方法快得多。