【问题标题】:Restrict API endpoint by total requests of each user按每个用户的总请求数限制 API 端点
【发布时间】:2019-10-01 17:23:34
【问题描述】:

我目前正在探索通过每月请求总数限制对 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,它会查看每个用户的“注册”时间戳,计算一个月是否已过并相应地重置分配的请求,或者是否有更好的方法来设计类似的东西这个?

【问题讨论】:

  • 1. req.user -> 在 mongodb 中创建“_id”,以便搜索和加载问题不会出现。 2. 不要使用日常的 cron 作业。而是在 mongodb 中添加一个字段来检查日期并将其与 1 个月每次请求进行比较。我很想知道是否有更好的解决方案。

标签: node.js express


【解决方案1】:

必须更新 MongoDB 文档的性能/可扩展性问题 每次有请求 - 这是否可行或我会遇到问题 应用何时增长?

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

重置计数 - 这应该是查看 每个用户的“注册”时间戳,如果是一个月,则计算 已通过并相应地重置分配的请求,或者是否存在 更好的方式来设计这样的东西?

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

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

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() */
    }   
}

当然,sn-p 是不完整的。它只是让您了解如何编写该中间件。

以下是您如何将中间件用于您的 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 */)

【讨论】:

  • 感谢您的回答-您因此使用什么策略将配额对象保存到数据库?你有另一个与数据库同步的每小时 cronjob 吗?
  • 是的,您可以运行 CRON 进行同步。不过要小心 CRON,从本质上讲,它往往会经常读写数据库。
【解决方案2】:

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/any store 的过多请求。

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2021-10-04
    • 1970-01-01
    • 2021-12-08
    • 2019-07-20
    • 2018-03-23
    • 1970-01-01
    • 1970-01-01
    • 2013-08-16
    相关资源
    最近更新 更多