چگونه با استفاده از Nginx در سیستم‌عامل Ubuntu 20.04 برای یک برنامه‌ی Node.js محدودیت نرخ (Rate Limit) اعمال کنیم؟

مقدمه

محدودکننده‌های نرخ (rate limiters) برای محیط‌های تولید ضروری هستند چون از دسترسی حملات brute-force مخرب به منابع حساس پشت فرم‌های ورود وب جلوگیری می‌کنند. آن‌ها همچنین در جلوگیری از حملات توزیع‌شدهٔ انکار سرویس (DDoS) نقش فعالی دارند. وب‌سرورهایی مانند Nginx ابزارهایی بومی برای پیاده‌سازی محدودسازی نرخ دارند و پردازش درخواست‌ها را با مصرف منابع کمتر انجام می‌دهند.

در این آموزش، Nginx را طوری پیکربندی خواهید کرد که میزان ترافیکی را که یک برنامهٔ سادهٔ Node.js برای ورود دریافت می‌کند، محدود کند. شما Nginx را برای تعریف حداکثر تعداد درخواست‌ها در یک بازهٔ زمانی مشخص پیکربندی می‌کنید، مقداردهی حد را آزمایش می‌کنید و صفحهٔ خطای 429 سفارشی برای کاربران تنظیم می‌کنید.

پیش‌نیازها

برای تکمیل این آموزش نیاز دارید به:

  • یک سرور Ubuntu 20.04 که Node.js روی آن نصب شده و دارای 1 CPU و 1GB رم است، با یک کاربر غیر ریشه که دسترسی sudo دارد و فایروال پیکربندی شده است. توجه داشته باشید که محدودکننده‌های Nginx به داشتن CPU بیشتر نسبت به RAM حساس‌ترند.
  • یک اپلیکیشن Node.js با مدیر پردازش PM2 که روی سرور شما تنظیم شده باشد.
  • Nginx نصب‌شده همراه با گواهی‌های SSL.
  • یک دامنهٔ ثبت‌شده. می‌توانید از خدماتی مثل Freenom دامنهٔ رایگان بگیرید یا از دامنهٔ موجود خود استفاده کنید.
  • یک رکورد DNS از نوع A که نام دامنهٔ شما را به آدرس IPv4 اپلیکیشن شما که پشت Nginx قرار دارد اشاره کند.
  • آشنایی پایه‌ای با HTML و CSS برای ساخت فرم ورود و صفحات خطا مفید خواهد بود.

نمای کلی

معمولاً محدودکننده‌های نرخ جلوی API یا اپلیکیشن قرار می‌گیرند. در این آموزش از یک اپلیکیشن Node.js استفاده می‌کنیم که یک فرم ورود ساده را پشت یک محدودکنندهٔ نرخ قرار می‌دهد.

گام اول — آماده‌سازی پروژه Node.js

ابتدا وابستگی‌های پروژه را نصب کنید، فرم ورود را ایجاد کنید (با استایل اختیاری) و اپلیکیشن را تست کنید. ابتدا یک پوشهٔ پروژه جدید ایجاد کنید که فایل‌های منبع و وابستگی‌های npm در آن ذخیره شوند.

یک پوشهٔ جدید برای فایل‌های اپلیکیشن ایجاد کنید:

sudo mkdir -p /srv/rate-limited-login
sudo chown sammy:sammy /srv/rate-limited-login
cd /srv/rate-limited-login
npm init

توضیح: مالکیت پوشه را به کاربر غیر ریشه (sammy) اختصاص می‌دهیم تا نیازی به استفادهٔ sudo برای ایجاد و ویرایش فایل در این دایرکتوری نباشد.

قبل از نوشتن کد، Express را به عنوان وابستگی نصب کنید تا مسیرهای HTTP را در اپلیکیشن Node.js مدیریت کند:

npm install express --save

حالا مسیر HTTP را ایجاد می‌کنیم که بعداً پشت محدودکننده قرار می‌گیرد. پوشهٔ پروژه (/srv/rate-limited-login) سه فایل خواهد داشت: index.js، پوشهٔ public و فایل‌های HTML/CSS داخل آن.

فایل index.js را بسازید و ویرایش کنید:

const express = require('express');
const app = express();

app.use(express.urlencoded({ extended: true }));
app.use(express.static('public'));

app.post('/', (req, res) => {
  res.send('Logging in...');
});

app.listen(8080, () => {
  console.log('Server running on port 8080');
});

این اپلیکیشن با اجرای index.js توسط Node.js، درخواست‌های HTTP را پردازش می‌کند. هر فایلی که در /srv/rate-limited-login/public قرار گیرد از طریق وب‌سرور قابل دسترسی خواهد بود و درخواست POST به http://localhost:8080 پاسخ Logging in… را بازمی‌گرداند.

یک پوشهٔ public برای میزبانی HTML و CSS فرم ورود ایجاد کنید:

mkdir public
cd public

یک فایل index.html ایجاد کنید:

<!doctype html>
<html lang="fa">
<head>
  <meta charset="utf-8">
  <title>Login</title>
  <link rel="stylesheet" href="login.css">
</head>
<body>
  <form method="POST" action="/">
    <label>Username:</label>
    <input type="text" name="username" />
    <label>Password:</label>
    <input type="password" name="password" />
    <button type="submit">Login</button>
  </form>
</body>
</html>

هر بار که کاربر فرم را ارسال کند، یک درخواست POST به مسیر / فرستاده می‌شود که در index.js تعریف شده است.

در صورت تمایل می‌توانید فایل CSS را بسازید (مثلاً login.css) تا استایل دلخواه را اضافه کنید:

/* Example CSS */
body {
  font-family: Arial, sans-serif;
  height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  background: linear-gradient(135deg, #71b7e6, #9b59b6);
}
form {
  background: rgba(255,255,255,0.9);
  padding: 20px;
  border-radius: 8px;
}

حالا همهٔ فایل‌های لازم را دارید و می‌توانید اپلیکیشن را اجرا کنید:

node index.js

برای متوقف کردن اجرای اپلیکیشن از CTRL+C استفاده کنید.

اجرای اپلیکیشن با PM2

اگر جلسهٔ SSH خود را ببندید، اپلیکیشن متوقف خواهد شد. برای نگه داشتن اپلیکیشن پس از خروج از سرور از PM2 استفاده کنید.

pm2 start index.js --name rate-limited-login
pm2 save

برای تست، یک درخواست POST به اپلیکیشن بفرستید:

curl -X POST http://localhost:8080

در عمل یک فرم ورود واقعی نیازمند امنیت‌های بیشتری است؛ برای راهنمایی‌های بیشتر به OWASP Authentication Cheat Sheet مراجعه کنید. همچنین می‌توانید از کتابخانه‌هایی مانند Passport برای احراز هویت استفاده کنید.

پیاده‌سازی محدودکننده نرخ در Nginx

در این مرحله Nginx ترافیک را به اپلیکیشن ورود هدایت می‌کند. از سه دستور Nginx استفاده خواهیم کرد: limit_req_zone، limit_req و limit_req_status.

limit_req_zone معیار محدودسازی درخواست‌ها، میزان حافظهٔ اختصاصی Nginx برای نگهداری سوابق و نرخ مجاز را تعیین می‌کند.

بلوک پیکربندی سایت را باز کنید (فایل مربوط به login.your_domain.com):

# open your server block configuration, e.g. /etc/nginx/sites-available/login.your_domain.com

یک limit_req_zone جدید اضافه کنید:

limit_req_zone $binary_remote_addr zone=login_limit:10m rate=5r/s;

خط بالا مشخص می‌کند که محدودسازی بر پایهٔ IP کلاینت ($binary_remote_addr) انجام شود، منطقه‌ای به نام login_limit با 10مگابایت حافظه ایجاد کند و نرخ مجاز 5 درخواست در ثانیه باشد.

برای محدودسازی بر اساس معیارهای دیگر می‌توانید از متغیرهایی مانند $geoip_country_code یا سایر متغیرهای Nginx استفاده کنید.

limit_req محدودهٔ کاربرد محدودسازی و گزینه‌هایی مانند burst و nodelay/delay را تعریف می‌کند.

server {
  ...
  location / {
    proxy_pass http://localhost:8080;
    limit_req zone=login_limit burst=10;
  }
}

دستور limit_req می‌تواند در context های server یا location قرار گیرد.

limit_req_status به طور پیش‌فرض Nginx هنگام عبور از حد آستانه کد 503 را برمی‌گرداند که می‌تواند گیج‌کننده باشد. با استفاده از limit_req_status می‌توانیم به جای 503، کد 429 (Too Many Requests) را بازگردانیم.

server {
  ...
  limit_req_status 429;
}

بعد از افزودن این دستورات پیکربندی شما چیزی مشابه زیر خواهد داشت:

limit_req_zone $binary_remote_addr zone=login_limit:10m rate=5r/s;

server {
  listen 80;
  server_name login.your_domain.com;

  location / {
    proxy_pass http://localhost:8080;
    limit_req zone=login_limit burst=10;
  }

  limit_req_status 429;
}

در انتها پیکربندی Nginx را ری‌بار کنید:

sudo nginx -t
sudo systemctl reload nginx

آزمایش محدودکننده نرخ با ابزار بارگذاری

برای تست، می‌توانید صفحات را در مرورگر رفرش کنید تا خطای 429 ببینید، اما برای شبیه‌سازی درخواست‌های همزمان از ابزارهایی مثل k6 یا Apache Benchmark (ab) استفاده کنید. در این آموزش از Apache Benchmark استفاده می‌کنیم چون CLI ساده و بدون نیاز به اسکریپت دارد.

نصب Apache Benchmark:

sudo apt update
sudo apt install apache2-utils

برای اجرا 100 اتصال همزمان و مجموع 100 درخواست:

ab -n 100 -c 100 https://login.your_domain.com/

در خروجی ab به تعداد پاسخ‌های خطا توجه کنید و همچنین زمان‌های پاسخ را بررسی کنید. با نرخ محدود 5r/s، حداکثر یک درخواست هر 200ms مجاز خواهد بود.

برای مشاهدهٔ لاگ خطاهای Nginx در زمان اجرای تست در ترمینال دیگری دستور زیر را اجرا کنید:

sudo tail -f /var/log/nginx/error.log

نمونهٔ لاگ:

2020/01/01 12:00:00 [error] 12345#0: *1 limiting requests, excess: 120.00 by zone "login_limit", client: 1.2.3.4, server: login.your_domain.com, request: "GET / HTTP/1.1", host: "login.your_domain.com"

در این لاگ: excess نشان‌دهندهٔ میزان مازاد درخواست نسبت به آستانه است، login_limit نام منطقهٔ محدودسازی است، client آدرس IP بازدیدکننده است و request نوع درخواست را نمایش می‌دهد.

تنظیمات پیشرفته: burst، nodelay و delay

با نرخ 5r/s (یک درخواست هر 200ms)، ممکن است برای وب‌سایتی که منابع متعددی مثل تصاویر و CSS بارگذاری می‌کند کافی نباشد. Nginx امکان تعیین صف burst را می‌دهد تا تعداد اضافی از درخواست‌ها را قبل از بازگرداندن خطا بپذیرد. اما صف ممکن است باعث ایجاد تاخیر در پردازش شود چون درخواست‌ها به نوبت سرو می‌شوند. nodelay اجازه می‌دهد تا درخواست‌های صف بدون انتظار بین هرکدام پردازش شوند. همچنین از Nginx 1.15.7 به بعد، delay ترکیبی از این دو را ارائه می‌دهد که به شما اجازه می‌دهد تعدادی از درخواست‌ها را بدون تاخیر عبور دهید و بقیه را با تاخیر مشخص شده پردازش کنید.

برای افزودن صف burst:

location / {
  proxy_pass http://localhost:8080;
  limit_req zone=login_limit burst=10;
}

ری‌لود Nginx:

sudo systemctl reload nginx

برای فعال‌سازی nodelay تا درخواست‌های صف بدون تاخیر پردازش شوند:

location / {
  proxy_pass http://localhost:8080;
  limit_req zone=login_limit burst=10 nodelay;
}

اگر بخواهید به جای nodelay از delay استفاده کنید (نسخهٔ Nginx ≥ 1.15.7)، مثلاً اجازه دهید تا 4 درخواست از صف بدون تاخیر عبور کنند:

location / {
  proxy_pass http://localhost:8080;
  limit_req zone=login_limit burst=10 delay=4;
}

در این حالت حداکثر 4 درخواست می‌توانند بدون تاخیر پردازش شوند و بقیهٔ 6 درخواست صف با حد مشخص (مثلاً 200ms بین درخواست‌ها) پردازش خواهند شد.

ایجاد صفحهٔ خطای 429 سفارشی

هر زمان که بازدیدکننده از حد مجاز فراتر رود، Nginx صفحهٔ خطای پیش‌فرض را نشان می‌دهد. برای بهبود تجربهٔ کاربر می‌توانید صفحهٔ 429 سفارشی‌ای مطابق طراحی سایت خود بسازید.

یک پوشه برای فایل‌های خطا بسازید و یک فایل 429.html ایجاد کنید:

sudo mkdir -p /usr/share/nginx/errors
sudo nano /usr/share/nginx/errors/429.html
<!doctype html>
<html lang="fa">
<head>
  <meta charset="utf-8">
  <title>Too Many Requests</title>
  <style>
    body { font-family: Arial, sans-serif; height:100vh; display:flex; justify-content:center; align-items:center; background: linear-gradient(135deg,#71b7e6,#9b59b6); color:#fff; }
    .card { background: rgba(0,0,0,0.4); padding: 30px; border-radius: 8px; text-align:center; }
    h1 { font-size: 48px; margin:0 0 10px 0; }
  </style>
</head>
<body>
  <div class="card">
    <h1>429</h1>
    <p>تعداد درخواست‌ها از حد مجاز فراتر رفته است. لطفاً دقایقی بعد تلاش کنید.</p>
  </div>
</body>
</html>

سپس در بلوک پیکربندی Nginx، مسیر خطا را تعریف کنید و پوشهٔ /usr/share/nginx/errors را به عنوان عمومی در دسترس قرار دهید:

server {
  listen 80;
  server_name login.your_domain.com;

  root /var/www/html;

  error_page 429 /errors/429.html;
  location /errors/ {
    alias /usr/share/nginx/errors/;
  }

  location / {
    proxy_pass http://localhost:8080;
    limit_req zone=login_limit burst=10;
  }

  limit_req_status 429;
}

ری‌لود Nginx:

sudo nginx -t
sudo systemctl reload nginx

برای دیدن صفحهٔ 429 می‌توانید به login.your_domain.com مراجعه کرده و پیکربندی‌ها را طوری تغییر دهید که سریع‌تر نرخ مجاز را رد کنید (مثلاً حذف burst یا کاهش مقدار rate) تا صفحهٔ خطای سفارشی نمایش داده شود.

نتیجه‌گیری

در این مقاله Nginx را طوری پیکربندی کردید که درخواست‌های ورودی به یک فرم ورود Node.js را محدود کند تا از دسترسی بیش از حد و حملات مخرب جلوگیری شود. می‌توانید این روش را برای افزایش امنیت اپلیکیشن‌های وب خود اعمال کنید.

گام بعدی: مستندات Nginx را برای Limiting Access to Proxied HTTP Resources بررسی کنید تا بیاموزید چگونه پهنای باند یا تعداد کانکشن‌ها را نیز محدود کنید. همچنین می‌توانید لیست دسترسی مبتنی بر جغرافیا بسازید. برای آخرین توسعه‌های Nginx به وبلاگ Nginx مراجعه کنید.

از اینکه با ParminCloud همراهید متشکریم.

Click to rate this post!
[Total: 0 Average: 0]

نظرات کاربران

دیدگاهی بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *