如何在应用程序平台上使用 Node.js 构建速率限制器

作者选择了 COVID-19 救助基金以作为 写给捐款计划的一部分接受捐款。

介绍

速度限制管理您的网络流量,并限制某人在特定时间内重复操作的次数,例如使用API。

在本教程中,您将构建一个 Node.js 服务器,该服务器将检查请求的 IP 地址,并通过比较每个用户的请求时间标签来计算这些请求的速度。如果 IP 地址超过您为应用程序设置的限制,您将拨打 Cloudflare API并将 IP 地址添加到列表中。

到本教程结束时,您将建立一个 Node.js 项目,部署在 DigitalOcean 的 应用平台上,该项目可以保护 Cloudflare 路由域名以限制率。

前提条件

在开始本指南之前,您将需要:

第1步:设置Node.js项目并部署到DigitalOcean的应用平台

在此步骤中,您将扩展到基本的 Express 服务器,将代码推到 GitHub 存储库,并将应用程序部署到 App Platform。

使用代码编辑器打开基本 Express 服务器的项目目录,在项目的 root 目录中创建一个名为 .gitignore’的新文件,在新创建的 .gitignore' 文件中添加以下行:

1[label .gitignore]
2node_modules/
3.env

您的.gitignore 文件中的第一个行是 git 指示不要跟踪 node_modules 目录. 这将允许您保持存储库大小小. 在需要时,可以通过运行 npm install 命令生成 `node_modules. 第二行防止环境变量文件被跟踪。

在代码编辑器中导航到server.js并修改以下代码行:

1[label server.js]
2...
3app.listen(process.env.PORT || 3000, () => {
4    console.log(`Example app is listening on port ${process.env.PORT || 3000}`);
5});

将条件使用PORT作为环境变量的更改使应用程序能够动态地在分配的PORT上运行服务器,或者使用3000作为倒退。

<$>[注] 注: console.log()中的字符串被包裹在 backticks(`)内,而不是引用中。这使您可以使用 template literals,这使您能够在字符串中使用表达式。

访问您的终端窗口并运行您的应用程序:

1node server.js

您的浏览器窗口将显示成功响应。在您的终端,您将看到以下输出:

1[secondary_label Output]
2Example app is listening on port 3000

随着您的 Express 服务器成功运行,您现在将部署到 App Platform。

首先,在工程的根目录中初始化 )并点击Create App 按钮. 选择** GitHub ** 选项, 必要时使用 GitHub 授权 。 从您要部署到 App 平台的下拉工程列表中选择您的项目仓库 。 审查配置,然后给出应用程序的名称. 为本教程目的,选择应用开发阶段的** 基本** 计划. 一旦准备好,点击** Launch App** .

接下来,导航到)记录,以便您将其添加到您的域名的 Cloudflare DNS 帐户中。

随着您的应用程序被部署到 App 平台, 在 Cloudflare 上的您域名的仪表板 在一个新的标签中, 您将返回 App 平台的仪表板 稍后。 导航到 DNS 标签。 点击** Add Record** 按钮,选择** CNAME** 作为您的** Type** ,将%%%作为根并粘贴在从App平台复制的CNAME中。 点击您 App 平台的 Dashboard 中的** Save ** 按钮,然后在** Settings ** 标签下导航到** Domain ** 区域,并点击** Add域** 按钮.

点击部署标签以查看部署的详细信息。部署完成后,您可以在浏览器中打开your_domain以查看它。您的浏览器窗口将显示:成功响应。在应用平台仪表板上的运行日志标签,您将看到以下输出:

1[secondary_label Output]
2Example app is listening on port 8080

<$>[注] 注: 端口号 8080 是 App Platform 默认分配的端口。

随着您的应用程序现在部署到 App Platform,让我们看看如何描述缓存以计算对率限制者的请求。

步骤 2 – 缓存用户的 IP 地址并计算每秒请求

在此步骤中,您将在_cache_中存储一个用户的IP地址,并带有一系列的时间戳来监视每个用户的IP地址每秒的请求. 一个缓存是应用程序经常使用的数据的临时存储. 缓存中的数据通常保存在像RAM(Random-Access Memory)这样的快速访问硬件中. 缓存的基本目标是通过减少访问其下较慢的存储层的需要来改进数据检索性能. 你们将使用三个npm软件包:node-cache'、is-ip'和`request-ip'来协助这一进程.

request-ip包捕获用户的 IP 地址,用于请求服务器。 node-cache包创建一个内存缓存,您将使用它来跟踪用户的请求。 您将使用 is-ip 包来检查 IP 地址是否为 IPv6 地址。 在您的终端上安装 node-cache, is-iprequest-ip 包。

1npm i node-cache is-ip request-ip

在代码编辑器中打开 server.js 文件,并在 const express = require('express'); 下方添加以下代码行:

1[label server.js]
2...
3const requestIP = require('request-ip');
4const nodeCache = require('node-cache');
5const isIp = require('is-ip');
6...

这里的第一行从你安装的request-ip'包取出requestIP'模块。 此模块可捕捉用户用于请求服务器的IP地址. 第二行取自 " 节点卡切 " 包中的 " 节点卡切 " 模块。 ) 标记 .

定义一组常数 变量在您的 server.js 文件中. 您将在整个应用程序中使用这些常数。

1[label server.js]
2...
3const TIME_FRAME_IN_S = 10;
4const TIME_FRAME_IN_MS = TIME_FRAME_IN_S * 1000;
5const MS_TO_S = 1 / 1000;
6const RPS_LIMIT = 2;
7...

TIME_FRAME_IN_S'是一个常数变量,将决定您应用程序平均用户时间戳的期间。 延长时间会增加缓存大小,从而消耗更多的内存. TIME_ FRAME_ IN_ MS常数变量也将决定您应用程序平均用户时间戳的时间段, 但以毫秒计 。MS_TO_S' 是您用以毫秒为单位转换时间的转换系数。 `RPS_LIMIT'变量是应用程序的阈值,该阈值将触发利率限制器,并按照应用程序的要求改变数值。 " RPS-LIMIT " 变量中的 " 2 " 是中值,将在开发阶段触发.

通过 Express,您可以编写和使用 middleware 函数,这些函数可以访问到您的服务器的所有 HTTP 请求。 要定义一个 middleware 函数,您将呼叫 app.use() 并传递一个函数。 创建一个名为 ipMiddleware 的函数作为 middleware。

 1[label server.js]
 2...
 3const ipMiddleware = async function (req, res, next) {
 4    let clientIP = requestIP.getClientIp(req);
 5    if (isIp.v6(clientIP)) {
 6        clientIP = clientIP.split(':').splice(0, 4).join(':') + '::/64';
 7    }
 8    next();
 9};
10app.use(ipMiddleware);
11
12...

requestIP'提供的GetClientIp ()'函数将中间软件中的请求对象req'取作参数。 .v6 ()'函数来自is-ip'模块,如果向其传递的论点是一个IPv6地址,则返回real'。 Cloudflare的"列表"要求IPv6地址在"64" CIDR注解. 您需要格式化IPv6地址以遵循格式 :aaaa:bbbb:cccc:ddd::64'. ['.split (':)](https://andsky.com/tech/tutorials/how-to-index-split-and-manipulate-strings-in-javascript)方法从字符串中创建出一个包含IP地址的阵列,由字符':'进行分割. [.splice (0,4)] (https://andsky.com/tech/tutorials/how-to-use-array-methods-in-javascript-mutator-methods) 方法返回数组的前四个元素。 .join ('):' 方法从数组中返回字符 :` 的字符串.

「next()」呼叫會指示中間軟件移動到下一個中間軟件函數,如果有,在您的例子中,它會將請求帶到 GET 路線 /

通过在常数下方添加以下变量来初始化一个节点缓存实例:

1[label server.js]
2...
3const IPCache = new nodeCache({ stdTTL: TIME_FRAME_IN_S, deleteOnExpire: false, checkperiod: TIME_FRAME_IN_S });
4...

使用恒定变量IPCache,您正在将原始的默认参数替换为nodeCache并使用自定义属性:

  • stdTTL':从缓存中取出一对缓存的密钥值的间隔数秒后。 TTL'代表_Time To Live_,是缓存到期时间的尺度。
  • 删除关于引渡 ' : 设定 , 因为您将写一个自定义的回调功能来处理 过期` 事件 。
  • " 周期 " : 触发过期元素自动检查的间隔( 秒) 。 默认值为`600',由于您的应用程序元素的过期设定为较低值,过期的核对也将提前进行。 .

对于关于 node-cache 的默认参数的更多信息,您将发现 node-cache npm 包的 docs 页面有用。以下图表将帮助您可视化缓存如何存储数据:

Schematic Representation of Data Stored in Cache

现在,您将为新 IP 地址创建一个新的密钥值对,并附加到现有的密钥值对中,如果在缓存中存在 IP 地址。 该值是与您的应用程序所提出的每个请求相符的时刻印的数组。 在您的 server.js 文件中,在 updateCache() 函数下方创建 IPCache 常数以添加缓存请求的时刻印:

1[label server.js]
2...
3const updateCache = (ip) => {
4    let IPArray = IPCache.get(ip) || [];
5    IPArray.push(new Date());
6    IPCache.set(ip, IPArray, (IPCache.getTtl(ip) - Date.now()) * MS_TO_S || TIME_FRAME_IN_S);
7};
8...

函数中第一行获得给定的IP地址的时间戳数组,如果无效,则以空数组初始化. 在下面一行中,您正在将被) " 功能由 " node-cache " 提供,这需要三个理由:key'、value'和TTL'。 此 TTL 将取代 IPCache 变量中的 stdTL值,从而取代标准 TTL 。 如果在缓存中已经存在 IP 地址, 您将使用现有的 TTL; 否则, 您将把 TTL 设置为 TIME_ FRAME_ IN_ S ` .

当前关键值对的 TTL 是通过从到期时间标签中扣除当前时刻标签来计算的,然后将差异转换为秒,并作为第三个参数传递给 .set() 函数..getTtl() 函数将一个键和 IP 地址作为参数,并将关键值对的 TTL 作为时刻标签返回。

注意:您需要从毫秒到秒的转换时间标签,因为JavaScript将其存储为毫秒,而节点缓存模块则使用秒。

在))`之后,添加以下行,以计算呼叫您的应用程序的IP地址每秒请求:

 1[label server.js]
 2...
 3    updateCache(clientIP);
 4    const IPArray = IPCache.get(clientIP);
 5    if (IPArray.length > 1) {
 6        const rps = IPArray.length / ((IPArray[IPArray.length - 1] - IPArray[0]) * MS_TO_S);
 7        if (rps > RPS_LIMIT) {
 8            console.log('You are hitting limit', clientIP);
 9        }
10    }
11...

第一行将 IP 地址请求的时间戳添加到缓存中, 通过调用您宣布的),且每秒请求数超过您在常数中定义的阈值,则您会"控制.log"IP地址. `rps' 变量通过除去时间间隔相差的请求数计算请求每秒,并将单位转换为秒.

由于您已在IPCache变量中默认设置deleteOnExpire属性为false,因此您现在需要手动处理过期事件。

1[label server.js]
2...
3IPCache.on('expired', (key, value) => {
4    if (new Date() - value[value.length - 1] > TIME_FRAME_IN_MS) {
5        IPCache.del(key);
6    }
7});
8...

.on()'是一种召回功能,它接受过期元素的关键'和价值'作为论据。 在您的缓存中,)'函数以key'作为参数,从缓存中删除过期元素.

对于数组中的某些元素至少在过去的时间比现在的时间TIME_FRAME_IN_S的例子,您需要通过从缓存中删除已过期的元素来处理它。

1[label server.js]
2...
3    else {
4        const updatedValue = value.filter(function (element) {
5            return new Date() - element < TIME_FRAME_IN_MS;
6        });
7        IPCache.set(key, updatedValue, TIME_FRAME_IN_S - (new Date() - updatedValue[0]) * MS_TO_S);
8    }
9...

filter () 阵列方法原生于 JavaScript , 它提供了一个调用函数来过滤您阵列中的时间戳中的元素 。 就你的情况而言,对过去最不)' 回调函数 。 TIME_FRAME_IN_S的相差点和第一个请求在更新的Value中的时间戳之后的相差点计算出新的和更新的TTL.

随着中间件功能的定义,请访问终端窗口并运行您的应用程序:

1node server.js

然后,在您的 Web 浏览器中访问localhost:3000。您的浏览器窗口将显示:成功响应。重复刷新页面以点击RPS_LIMIT

1[secondary_label Output]
2Example app is listening on port 3000
3You are hitting limit ::1

注意: localhost 的 IP 地址显示为 ::1. 您的应用程序在 localhost 以外部署时会捕获用户的公共 IP。

您的应用程序现在可以跟踪用户的请求并将时刻印记存储在缓存中,在下一步,您将集成Cloudflare的API来设置防火墙。

第3步:设置Cloudflare防火墙

在此步骤中,您将设置 Cloudflare 防火墙,以阻止 IP 地址在打击率限制时,创建环境变量,并对 Cloudflare API 进行调用。

请访问您的浏览器中的 Cloudflare 仪表板 登录并导航到您的帐户主页。 打开 Lists 在** Configurations** 页面下。 创建一个新的列表以名称为 your_list

注意: 列表 部分可在您的 Cloudflare 帐户的仪表板页面上,而不是您的 Cloudflare 域的仪表板页面上。

导航到)(Create a Firewall rule)(Create a Firewall rule)(Create a Firewall rule)(Rules)(Rules)(Rules)(Rules)(Rules)(Rules)(Rules)(Rules)(Rules)(Rules)(Rules)(Rules)(Rules)(Rules)(Rules)(Rules)(Rules)(Rules)(Rules)(Rules)(Rules)(Rules)(Rules)(Rules)(Rules)(Rules)(Rules)(Rules)(Rules)(Rules)(Rules)(Rules)(Rules)(Rules)(Rules)(Rules)(Rules)(Rules

在项目的根目录中创建一个 .env 文件,使用以下行来从您的应用程序中调用 Cloudflare API:

1[label .env]
2ACCOUNT_MAIL=your_cloudflare_login_mail
3API_KEY=your_api_key
4ACCOUNT_ID=your_account_id
5LIST_ID=your_list_id

要获得 API_ KEY 的值, 请在您的 Cloudflare 仪表板上浏览 API Tokens 标签 。 点击 ** 在 Global API 密钥中查看** ,并输入您的 Cloudflare 密码查看 。 访问账号主页上** Figures** 标签下的** Lists** 栏. 在您创建的 您的列表 列表外点击** Edit ** 。 在浏览器中取取ACCOUNT_ID ' 和LIST_ID ' 从您名单 ' 的网址取出。 URL格式如下: https://dash.cloudflare.com/你的账户_id/configurations/lists/你的_list_id`

<$>[警告] 警告: 请确保 .env 的内容保持保密,而不是公开。

安装 axiosdotenv 包通过 npm 在您的终端。

1npm i axios dotenv

在代码编辑器中打开server.js文件,并在nodeCache常数下添加以下代码行:

1[label server.js]
2...
3const axios = require('axios');
4require('dotenv').config();
5...

这里的第一行从您安装的axios包中获取axios模块. 您将使用此模块对 Cloudflare 的 API 进行网络调用. 第二行需要并配置dotenv模块以启用process.env全球变量,该变量将将您在您的.env文件中放置的值定义为server.js

在)条件中,在console.log('You are hitting limit', clientIP)」上方添加以下内容,以调用 Cloudflare API。

 1[label server.js]
 2...
 3    const url = `https://api.cloudflare.com/client/v4/accounts/${process.env.ACCOUNT_ID}/rules/lists/${process.env.LIST_ID}/items`;
 4    const body = [{ ip: clientIP, comment: 'your_comment' }];
 5    const headers = {
 6        'X-Auth-Email': process.env.ACCOUNT_MAIL,
 7        'X-Auth-Key': process.env.API_KEY,
 8        'Content-Type': 'application/json',
 9    };
10    try {
11        await axios.post(url, body, { headers });
12    } catch (error) {
13        console.log(error);
14    }
15...

您现在通过 URL 调用 Cloudflare API 来将一个项目添加到 IP 地址中 。 Cloudflare API将您的ACCOUNT_Mail'和API_KEY'放在请求的正文中,用键作为-Auth-Email'和-Auth-Key'。 请求正文将一系列以ip'作为IP地址的物体添加到列表中,并用你的评论'作为)'提出的POST请求被包裹在一个试捕区块中,以便处理可能出现的任何错误。 axios.post'函数取url'、body'和以headers'为对象提出请求.

在测试使用测试 IP 地址的 API 请求时,在ipMiddleware函数中更改clientIP变量,例如198.51.100.0/24,因为Cloudflare 在其列表中不接受 localhost 的 IP 地址。

1[label server.js]
2...
3let clientIP = '198.51.100.0/24';
4...

访问您的终端窗口并运行您的应用程序:

1node server.js

然后,在您的 Web 浏览器中访问localhost:3000。您的浏览器窗口将显示:成功响应。重复刷新页面以点击RPS_LIMIT

1[secondary_label Output]
2Example app is listening on port 3000
3You are hitting limit ::1

当你达到限制时,打开Cloudflare仪表板并导航到your_list的页面.你会看到你在添加到Cloudflare的列表中添加的代码中输入的IP地址,名为your_list

警告:在部署或推送代码到 GitHub 之前,请确保在您的)`。

通过执行修改和将代码推向 GitHub 来部署您的应用程序 。 由于您已经设置了自动部署, GitHub 的代码将自动部署到您的 DigitalOcean 的 App 平台 。 由于您的 .env 文件没有添加到 GitHub 中, 您需要通过 App- Level Environment Variables 的 ** Settings** 选项卡将其添加到 App 平台 。 从您的工程的 . env 文件添加密钥值对, 这样您的应用程序就可以访问 App 平台上的内容 。 在保存环境变量后,在部署完成后,在浏览器中打开你的域并反复刷新页面以点击RPS_LIMIT。 一旦达到极限,浏览器会显示Cloudflare的防火墙页面.

Cloudflare's Error 1020 Page

导航到 App Platform 仪表板上的 Runtime Logs 选项卡,您将看到以下输出:

1[secondary_label Output]
2...
3You are hitting limit your_public_ip

您可以从不同的设备或通过VPN打开your_domain,以查看防火墙只禁止在your_list中的IP地址。

注意:有时,由于浏览器的缓存响应,消防墙需要几秒钟才能触发。

您已设置 Cloudflare 防火墙以阻止 IP 地址,当用户通过拨打 Cloudflare API 来达到率限制。

结论

在本文中,你建立了一个Node.js项目部署在DigitalOcean的应用平台上,连接到通过Cloudflare路由的域名。你通过在Cloudflare上配置防火墙规则来保护你的域名免受率限制滥用。从这里,你可以修改防火墙规则以显示JS挑战或CAPTCHA,而不是禁止用户。The Cloudflare文档详细介绍了过程。

Published At
Categories with 技术
comments powered by Disqus