如何使用Strapi和Redis构建API请求
#strapi #redis #apirequest

我们的系统越复杂,我们的流程和请求就越多。从人类容易发生的错误中转移,我们会发现自己正在处理用户与系统互动的意外方式,或对我们项目互动的外部API的限制。此外,我们可能会选择控制用户如何与这些资源进行交互。

本教程向您展示了如何构建符合请求的API。我们将使用strapi实施博客应用程序,以显示如何将客户端请求限制为使用REDIS的特定端点。在此过程中,我们将发现和使用redis,了解请求对象包含的内容,并拦截针对我们的应用程序提出的请求。

先决条件

对于一种实用的方法来请求节流,我们将使用strapi和redis实施博客应用程序。因此,在开始之前,您应该在本地安装以下内容:

  1. 对JavaScript和Node.js的理解。
  2. Redis在主机计算机上安装并运行。
  3. Strapi的基本理解。

请求节流-429

请求节流是限制客户端可以在给定时间内向服务器提出的请求数量的过程。它是控制特定客户端使用的资源利用的一种手段,服务器在客户端义务 - 客户端面前的服务器时优先。我们这样做的原因有几个:

  1. 我们要防止服务器过载:执行数据操纵的常见资源密集型请求,例如针对CPU或RAM的图像处理。
  2. 我们的产品成本应保留在检查中:与Google Maps,LinkedIn等第三方API互动时通常是这种情况。
  3. 防止服务器滥用 - 拒绝服务攻击,DOS:例如,我们的服务器应用程序根据用户的IP地址返回有关位置的天气信息。为了防止任何客户使用的任何客户对此API的多次调用,我们可以限制每个时间段内的请求计数。

因此,我们将多个并发请求作为a 429,这意味着在时间范围内提出了太多请求。

润热的方法

  • 硬油门 - 请求永远不会达到油门限制。
  • 柔软的节流 - 请求的数量可以超过油门限制的特定百分比。
  • 动态节流 - 即使达到了请求限制,也可以处理请求,只要服务器具有可用资源。

节流算法

a)固定窗口算法
简单地说,在给定的期间,例如0800-0805小时内,我们的算法将限制请求数,而不管请求是在0800或0804hrs开始的5分钟提供。


b)滑动窗口算法
当针对我们的服务器提出请求时,我们计算针对初始请求的次数。我们没有将所有信任放在时间戳中,而是在提出初始请求时开始时间计数器。

随着上述内容,我们具有漏水的计数器或令牌桶算法,可以用作节流算法,但脱离了该部分的范围。

什么是雷迪斯?

Redis是一种内存数据存储,通常用作缓存或数据库。它是为高性能而设计的,可以快速存储和检索数据。我们可以将会话数据存储在Web应用程序中,缓存经常访问数据,并实现实时功能,例如聊天和消息传递。它也通常用作分布式系统中的消息经纪,就像RabbitMQ一样。

项目设置 - 脚手架我们的Strapi项目

让我们继续前进并脚打个脚步。

    npx create-strapi-app blog-api --quickstart
    # OR
    yarn create strapi-app blog-api --quick start

使用以上任何一个将生成一个strapi项目,即当前工作目录中的blog-api,并在默认端口http://localhost:1337/admin.中启动我们的nodejs项目。

继续,创建您的管理帐户并打开仪表板。
Strapi Admin Dashboard

创建一个集合和博客API

因为这是一个简单的博客应用程序,我们将创建一个包含文章的集合类型的架构。我们的文章将具有标题及其内容,包括类型文本。

New Collection Type

对于该领域,例如标题,命中并添加名称。您可以从高级设置选项卡中修改标题为唯一且需要。

Advanced Settings

我们为 content 字段做同样的事情,只是这次,我们将其指定为一个长文本字段。我们应该以下面的文章内容类型。

Article Content Type

点击保存,等待服务器重新启动并创建文章。

默认情况下,Strapi创建了一个用户集合类型(毗邻文章集合类型)。基于此,让我们创建一个新的条目,对于本指南,将此用户添加到 authenticated 角色。

User Collection Type

要使用API​​和作为身份验证的用户获取文章,我们需要让管理员API访问文章。

导航到设置,用户和权限插件>角色并修改身份验证的角色以访问所有文章,并且在读取模式下都可以访问所有文章。

Role Permissions

所有人都说并完成了,启动您的邮递员或您对API进行测试的任何客户。

Postman Setup

创建一个集合并命名 strapi Tests

Strapi Tests Collection

命中保存,然后导航到您创建的集合以创建请求。我们将创建两个端点:

  1. 使用URL http://localhost:1337/api/auth/local登录。
  2. 带有URL的文章:http://localhost:1337/api/articles

对于登录身份验证,我们的参数将如下所示,标识符和密码分别是用户名和密码。继续并验证您的用户。

API Authentication

之后,请点击API,以使用用户的jwt作为携带者令牌查看您的文章。

了解中间件 - 拦截请求

中间件是您的业务逻辑,后端和客户端应用程序之间的代码。其目的是执行诸如日志记录(用户正在做什么),缓存或请求限制之类的任务。

在Strapi中,我们有两种中间件:全局Strapi中间件和路线级中间件。可以找到有关此差异的更多详细信息。

让我们专注于路由中间件,并限制用户从我们的页面上查询文章的次数。

    npx strapi generate

结果如下:

Strapi Generator

选择中间件,然后继续填写其详细信息:

Select Middleware

生成的文件应如下:

    // src/api/article/middlewares/request-limiter.js
    'use strict';

    /**
     * `request-limiter` middleware
     */

    module.exports = (config, { strapi }) => {
      // Add your own logic here.
      return async (ctx, next) => {
        strapi.log.info('In request-limiter middleware.');

        await next();
      };
    };

在我们的请求达到数据库之前,我们将在 Artical 的API路线上放置中间件(速率限制者)。因此,我们从其默认值转换了我们的路线 src/api/api/article/artip.js

    "use strict";

    /**
     * article router
     */

    const { createCoreRouter } = require("@strapi/strapi").factories;

    module.exports = createCoreRouter("api::article.article");

到包含我们中间件的版本:

    "use strict";

    /**
     * article router
     * src/api/article/routes/article.js
     */

    const { createCoreRouter } = require("@strapi/strapi").factories;

    module.exports = createCoreRouter("api::article.article", {
      config: {
        find: {
        /* 
        where the array below specifies the path of the request-limiter file,
        src/api/article/middlewares/request-limiter.js , exclulding
        the middleware section of the path
        */
          middlewares: ["api::article.request-limiter"],
        },
      },
    });

以上意味着我们的API称为find返回文章列表时,我们的自定义中间件都会被调用。您可以将其修改为用于其他任何方法,例如findOnecreateupdatedelete,它们都是the core routers的一部分。

您还可以使用带有strapi的CLI找出中间件的路径。

    npx strapi middlewares:list

重复我们呼叫 api/articles 将显示 info log:在我们的终端中的请求限制性中间件中。

将Redis与Strapi集成

安装了REDIS,我们需要在IT和节点应用程序之间进行客户端。因此,在Redis的社区驱动节点客户端ioredis中。我们还继续安装Moment.js,这是一个javascript库。

    npm i ioredis moment

典型的请求将包含标头,方法,车身,参数和用户对象,以提及一些细节。在strapi中,这是连接到ctx.request对象的,并从控制器或策略中调用(可以看到更多信息here)。

回到请求限制器,我们的焦点将是ctx,这是一个带有所有请求信息的上下文对象。在此中,我们将寻找附加到请求的用户,并根据我们的API允许的请求计数修改响应对象。我们将回答:

  1. 用户在给定时间段内应提出多少个请求?
  2. 当他们超过此计数时,我们应该给出什么回应?

我们将使用REDIS根据其请求计数来存储用户ID。对于样板,我们尝试根据REDIS的ID来获取用户,并围绕其执行操作。如果某些事情失败了,我们会发现错误。

    'use strict';

    /**
     * `request-limiter` middleware
     */

    const redis = require("ioredis");
    const redisClient = redis.createClient();
    const moment = require("moment");

    module.exports = (config, { strapi }) => {
      return async (ctx, next) => {
        try {
          strapi.log.info("In request-limiter middleware.");
          // check if redis key exists
          // will return 1 or 0
          const userExists = await redisClient.exists(ctx.state.user.id);
          if (userExists === 1) { 
            strapi.log.info("User exists in redis.");
          }
          else {
            strapi.log.info( "User does not exist in redis.");
          }

          await next();
        } catch (err) {
          strapi.log.error(err);
          throw err;
        }

      };
    };

请注意以下行,我们使用Redis根据其ID检查存在用户的存在。

    const userExists = await redisClient.exists(ctx.state.user.id)

用户击中服务器后,我们检查是否有他们这样做的记录。如果这样做,我们会在时间段(1分钟)内检查它们的请求计数。让我们从用户存在时会发生的事情开始。

对于此实现,我们将使用固定窗口算法;用户可以在给定的时期内击中我们的服务器 x 次。

    strapi.log.info("User exists in redis.");
    const reply = await redisClient.get(ctx.state.user.id);
    const requestDetails = JSON.parse(reply);
    const currentTime = moment().unix();
    const time_difference = (currentTime - requestDetails.startTime) / 60;

    // reset the count if the difference is greater than 1 minute
    if (time_difference >= 1) {
      const body = {
        count: 1,
        startTime: moment().unix(),
      };
      await redisClient.set(ctx.state.user.id, JSON.stringify(body));
      next();
      // increment the count if the difference is less than 1 minute
      // where 10 is the number of requests we allow within the time frame
    } else if (time_difference < 1 && requestDetails.count <= 10) {
      requestDetails.count++;
      await redisClient.set(
        ctx.state.user.id,
        JSON.stringify(requestDetails)
      );
      next();
      // return error if the difference is less than 1 minute and count is greater than 3
    } else {
      strapi.log.error("throttled limit exceeded...");
      ctx.response.status = 429;
      ctx.response.body = {
        error: "Unable to process request",
        message: "throttled limit exceeded...",
      };
      return;
    }

现在已经知道我们的用户已经存在,我们获取并解析了附加的请求历史记录。我们还使用momentjs来获得当前时间。基于此,我们从他们初始请求的开始时间开始获得区别,并将其除以 60 将其转换为秒。

有三种方法可以解决:

  1. 初始请求和当前请求之间的时间为一分钟。
  2. 初始请求和当前请求之间的时间少于一分钟,并且请求计数也小于阈值。
  3. 如果未满足上述所有条件,则用户击中了我们的终点,超过了我们在集合集(1分钟)中允许的阈值。

在第一种情况下,我们重置用户的请求信息,并再次从1个开始计数。在第二个中,我们将简单地更新此用户的请求信息并更新其请求计数;在第三个中,我们让他们知道他们已经达到了极限。

如果我们的用户不存在怎么办?
现在,我们必须考虑到我们的内存商店Redis没有该请求的记录。在这种情况下,我们创建了一个带有ID的新键值对,请求计数(以第一个请求为首发),当前的UNIX时间为开始时间。

    // user does not exist. Add a new key-value pair with count as 1

    const body = { count: 1, startTime: moment().unix() };
    await redisClient.set(ctx.state.user.id, JSON.stringify(body));

结果

总的来说,我们的限制器将如下所示,将请求计数重构为全球常数。

    'use strict';

    /**
     * `request-limiter` middleware
     */

    const THROTTLE_LIMIT = 3;
    const redis = require("ioredis");
    const redisClient = redis.createClient();
    const moment = require("moment");

    module.exports = (config, { strapi }) => {
      // Add your own logic here.
      return async (ctx, next) => {
        try {
          // check if redis key exists
          const userExists = await redisClient.exists(ctx.state.user.id);
          if (userExists === 1) { 
            const reply = await redisClient.get(ctx.state.user.id);
            const requestDetails = JSON.parse(reply);
            const currentTime = moment().unix();
            const time_difference = (currentTime - requestDetails.startTime) / 60;

            // reset the count if the difference is greater than 1 minute
            if (time_difference >= 1) {
              const body = {
                count: 1,
                startTime: moment().unix(),
              };
              await redisClient.set(ctx.state.user.id, JSON.stringify(body));
              next();
              // increment the count if the time_difference is less than 1 minute
            } else if (time_difference < 1 && requestDetails.count <= THROTTLE_LIMIT) {
              requestDetails.count++;
              await redisClient.set(
                ctx.state.user.id,
                JSON.stringify(requestDetails)
              );
              next();
              // return error if the time_difference is less than 1 minute and count is greater than 3
            } else {
              strapi.log.error("Throttled limit exceeded...");
              ctx.response.status = 429;
              ctx.response.body = {
                error: 1,
                message: "Throttled limit exceeded...",
              };
              return;
            }
          }
          else {
            strapi.log.info("User does not exist in redis.");
            const body = {
              count: 1,
              startTime: moment().unix(),
            };
            await redisClient.set(ctx.state.user.id, JSON.stringify(body));
            next();
          }
        } catch (err) {
          strapi.log.error(err);
          throw err;
        }

      };
    };

测试请求限制器

您可以再次导航到Postman,进行身份验证并再次击中/api/articles 端点。由于多个请求超出了限制,并且在同一分钟内,您将在下面收到一个错误消息 429。

    {
        "error": "Unable to process request",
        "message": "throttled limit exceeded..."
    }

结论

我们创建了一个博客API,经过身份验证的用户,通过请求对象进行了查看,创建了自定义中间件以拦截其请求,并从此用户的同时请求有限。

供参考,您可以在marvinkweyu/blog-api上阅读本文的代码。

在这里了解更多信息:

Strapi Enterprise Edition