如何使用Node.js的速率限制

941 阅读4分钟

开始使用Node.js速率限制

速率限制功能使我们有可能保护后端API免受恶意攻击。它允许我们对用户向我们的API发出的请求的数量进行限制。

速率限制被API供应商广泛使用,以限制未订阅的用户在给定时间内的请求数量,例如,https://newsapi.org,限制拥有开发者账户的用户每天只能发出100个请求。

速率限制是一种用于控制服务器中传出和传入请求的功能。我们可能会限制一个没有高级账户的用户在一小时内的请求数为100。当一个用户在窗口时间内发出的请求超过所提供的限制时,就会返回一个错误信息,通知用户已经超过了允许的限制。

前提条件

要继续学习本教程,你应该具备以下条件。

  1. 对HTTP请求/响应的一般理解。
  2. 在你的电脑上安装了Node.jsRedis
  3. Node.js有一定了解。

项目设置

  1. 为该项目创建一个名为limiter 的文件夹。

  2. 在该目录中,执行下面的命令来初始化一个Node.js项目。

    $ npm init
    
  3. 在项目目录下安装express

    $ npm install --save express
    
  4. 在项目目录下,创建名为index.js 的入口文件,在其中添加以下代码片段。

    const express = require("express");
    const app = express();
    const port = 3000;
    //returns the string Hello World when / is visited
    app.get("/", (req, res) => {
      res.send("Hello World!");
    });
    
    app.listen(port, () => {
      console.log(`Example app listening at http://localhost:${port}`);
    });
    
  5. 在项目目录下执行下面的命令,以确保我们的应用程序运行时没有错误。

    $ node index.js
    

所提供的代码包含一个API,当我们向/posts 端点发送一个GET 请求时,会返回一个书籍列表。我们要实现一个速率限制器,将API的访问限制在指定时间内的特定数量的请求。我们将使用一个中间件层来实现速率限制器。

在项目根目录下,创建一个新文件,命名为routes.js ,并添加以下代码。

const { json } = require("express");
const express = require("express");
const router = express.Router();
const posts = [
  {
    id: 1,
    author: "Lilian",
    title: "Stock market",
    body: "Post 1",
  },

  {
    id: 2,
    author: "Tom",
    title: "Covid 19",
    body: "Post 2",
  },

  {
    id: 3,
    author: "Vincent",
    title: "Django APIs",
    body: "Post 3",
  },

  {
    id: 4,
    author: "Cindy",
    title: "Node.js Streams",
    body: "Post 4",
  },
];
router.get("/", function (req, res, next) {
  res.json(posts);
});

module.exports = router;

在上面的代码中,我们创建一个包含所有帖子的posts 数组。然后,路由器以JSON数组的形式返回一个posts

我们最后导出路由器,使其有可能在我们的index.js 文件中导入和使用。

使用第三方库实现速率限制器

express-rate-limiter 是一个用于Node.js中API速率限制的npm包。为了在我们的应用程序中使用它,我们必须安装它。

执行下面的命令,在我们的应用程序中安装express-rate-limiter

$ npm install express-rate-limit --save

index.js 文件中,添加以下代码。

const express = require("express");
const indexRoute = require("./router");
const rateLimit = require("express-rate-limit");
const app = express();
const port = 3000;

app.use(
  rateLimit({
    windowMs: 12 * 60 * 60 * 1000, // 12 hour duration in milliseconds
    max: 5,
    message: "You exceeded 100 requests in 12 hour limit!",
    headers: true,
  })
);

app.use("/posts", indexRoute);

app.listen(port, () => {
  console.log(`Example app listening at http://localhost:${port}`);
});
  • windowMs 是窗口的大小。在我们的例子中,我们使用了24小时的窗口时间,单位是毫秒。
  • max 是用户在一个给定的窗口持续时间内所能发出的最大请求量。
  • message 是用户在超过限制时得到的响应信息。
  • headers 是指是否添加标题以显示总的请求数以及在尝试再次提出请求之前的等待时间。

万岁!🥳我们已经在我们的API中实现了速率限制器。

使用Redis实现一个自定义的速率限制器

在这一节中,我们将使用Redis实现一个自定义的速率限制器,以存储每个用户的IP地址与该用户在窗口持续时间内的请求数量。

我们需要两个包来实现我们的自定义速率限制器,redis ,使我们能够连接到Redis,moment ,使我们能够操纵Javascript日期。

执行这个命令,将momentredis 包安装到我们的应用程序中。

$ npm install --save redis moment

在项目根目录下,创建一个名为customLimitter.js 的文件。在其中添加以下代码。

import moment from 'moment';
import redis from 'redis';

const redis_client = redis.createClient();
const WINDOW_DURATION_IN_HOURS = 24;
const MAX_WINDOW_REQUEST_COUNT = 100;
const WINDOW_LOG_DURATION_IN_HOURS = 1;


export const customLimiter = (req, res, next) => {
    try {
        //Checks if the Redis client is present
        if (!redis_client) {
            console.log('Redis client does not exist!');
            process.exit(1);
        }
        //Gets the records of the current user base on the IP address, returns a null if the is no user found
        redis_client.get(req.ip, function(error, record) {
            if (error) throw error;
            const currentTime = moment();
            //When there is no user record then a new record is created for the user and stored in the Redis storage
            if (record == null) {
                let newRecord = [];
                let requestLog = {
                    requestTimeStamp: currentTime.unix(),
                    requestCount: 1
                };
                newRecord.push(requestLog);
                redis_client.set(req.ip, JSON.stringify(newRecord));
                next();
            }
            //When the record is found then its value is parsed and the number of requests the user has made within the last window is calculated
            let data = JSON.parse(record);
            let windowBeginTimestamp = moment()
                .subtract(WINDOW_DURATION_IN_HOURS, 'hours')
                .unix();
            let requestsinWindow = data.filter(entry => {
                return entry.requestTimeStamp > windowBeginTimestamp;
            });
            console.log('requestsinWindow', requestsinWindow);
            let totalWindowRequestsCount = requestsinWindow.reduce((accumulator, entry) => {
                return accumulator + entry.requestCount;
            }, 0);
            //if maximum number of requests is exceeded then an error is returned
            if (totalWindowRequestsCount >= MAX_WINDOW_REQUEST_COUNT) {
                res
                    .status(429)
                    .jsend.error(
                    `You have exceeded the ${MAX_WINDOW_REQUEST_COUNT} requests in ${WINDOW_DURATION_IN_HOURS} hrs limit!`
                );
            } else {
                //When the number of requests made are less than the maximum the a new entry is logged
                let lastRequestLog = data[data.length - 1];
                let potentialCurrentWindowIntervalStartTimeStamp = currentTime
                    .subtract(WINDOW_LOG_DURATION_IN_HOURS, 'hours')
                    .unix();
                //When the interval has not passed from the last request, then the counter increments
                if (lastRequestLog.requestTimeStamp > potentialCurrentWindowIntervalStartTimeStamp) {
                    lastRequestLog.requestCount++;
                    data[data.length - 1] = lastRequestLog;
                } else {
                    //When the interval has passed, a new entry for current user and timestamp is logged
                    data.push({
                        requestTimeStamp: currentTime.unix(),
                        requestCount: 1
                    });
                }
                redis_client.set(req.ip, JSON.stringify(data));
                next();
            }
        });
    } catch (error) {
        next(error);
    }
};

在上面的代码中,我们在我们的应用程序中添加了Redismoment 。我们使用Redis 作为内存中的数据库来跟踪用户活动,而moment 帮助我们操作Javascript日期。

customLimiter 中间件包含跟踪用户活动的逻辑,并将其保存在 。Redis

测试

如果我们在localhost:3000/posts ,发出一个GET 的请求,我们将得到一个响应。

结论

你已经学会了如何在Node.js应用程序中实现速率限制,以及如何在你的应用程序中实现速率限制器来控制流量。