برنامه‌نویسی

آموزش کار با Middleware در فریم‌ورک Express.js


۱۸ اسفند ۱۳۹۹
آموزش کار با middleware در فریم‌ورک express.js

در این مقاله قصد داریم به شما نحوه‌ی کار با Middleware در فریم‌ورک Express.js را آموزش دهیم و همچنین به‌سراغ Middlewareهایی می‌رویم که برای راه‌اندازی اکثر برنامه‌های وب ضروری هستند. نحوه‌ی استفاده از هر کدام را بررسی خواهیم کرد و شما می‌توانید براساس نیاز خود، آن‌ها را پیکربندی کنید.

Middleware چیست؟

Middleware بخشی از فریم‌ورک Express.js است که در طول request-response cycle یعنی زمان دریافت request در سرور تا زمان ارسال response، اجرا می‌شود و به آبجکت‌های request و response دسترسی دارد.

حال Express middleware در انواع و سطح‌های مختلفی ارائه می‌شود:

و توسعه‌دهندگان به دلیل عملکرد محدود Express.js در برنامه‌های توسعه داده شده با این فریم‌ورک از Middlewareها برای پیاده‌سازی عملکردهای مختلف استفاده می‌کنند. شما می‌توانید Middleware خود را برای Express.js توسعه دهید اما اکثر توسعه‌دهندگان ترجیح می‌دهند از ابزارهای built-in و third-party برای عمده‌ی کارها استفاده کنند. به‌همین منظور نحوه‌ی استفاده از پنج Express middleware محبوب را در ادامه‌ی این مقاله آموزش می‌دهیم اما قبل از آن بایستی به نحوه‌ی کار Middleware در برنامه‌ بپردازیم.

نحوه‌ی کار Middleware در برنامه

برای درک نحوه‌ی کار Middleware در برنامه فرض کنید که شما لیموناد درست می‌کنید اما لیموترش‌های مورد نیاز برای تهیه‌ی لیموناد را خود مشتریان برای شما می‌آورند بنابراین شما مسئول ارزیابی طراوت و سلامت لیموها، دور ریختن لیمو‌های نامناسب و درنهایت تهیه لیموناد هستید.

حال برای اینکه فشار کاری خود را کاهش دهید، محمد را استخدام می‌کنید و از او می‌خواهید تا مطمئن شود که لیموهایی تهیه شده توسط مشتریان، ارگانیک و تازه باشد. در این مثال، محمد یک Middleware میان شما و لیموترش‌هایی است که مشتریان برای شما می‌آورند.

فرض کنیم که این کار برای شما سودآور بوده است، بنابراین دو کارگر دیگر با نام‌های علی و رضا استخدام می‌کنید. محمد مسئول رسیدگی به ارگانیگ بودن لیموها می‌شود و لیموهای ارگانیک را به علی می‌دهد تا لیموهای پوسیده را دور ریخته و لیموهای مناسب به دست رضا برسد تا طراوت آن‌ها را تایید کند. درنهایت شما لیمو‌های تایید شده توسط رضا را برای درست کردن لیمونادها استفاده می‌کنید.

در این صورت است که شما می‌توانید روی تهیه‌ی لیمونادها و افزایش سود خود تمرکز کنید.

برای تبدیل این مثال به یک مثال در حوزه‌ی فناوری می‌توان لیموها را درخواست‌های HTTP و لیموناد را Server دانست. قبل از تایید یا رد کردن یک درخواست HTTP بایستی منشا آن بررسی شود زیرا نمی‌توانیم به همه‌ی درخواست‌‌ها حتی اگر از یک منبع معتبر باشند، اعتماد کنیم بنابراین به فیلتر کردن آن‌‌ها می‌پردازیم.

اگر صحت یک درخواست HTTP توسط هر کدام از Middlewareها تایید نشد، می‌توانیم آن درخواست را از چرخه‌ی request و response خارج کنیم. همچنین درخواست‌هایی که از Middlewareهای برنامه‌های شما عبور می‌کنند، دراختیار Controller برنامه قرار داده می‌شوند.

مثال تصویری برای درک نحوه‌ی کار middleware

البته این یک مثال بسیار ساده بود. مطمئنا در یک سناریو واقعی ممکن است از چندین Middleware برای انجام یک کار واحد مانند ورود کاربر به سیستم استفاده شود.

راه‌اندازی یک API ساده با Express.js

برای آموزش نحوه‌ی استفاده از Middlewareها به یک Express API ساده نیاز داریم که در ادامه‌ی این بخش به توسعه و راه‌اندازی آن می‌پردازیم.

دستورهای زیر را به‌ترتیب در Terminal سیستم‌عامل خود وارد کنید:

mkdir express-api
cd express-api
npm init -y

دستور آخر یعنی npm init -y یک فایل با نام package.json در مسیر فعلی شما یعنی express-api ایجاد می‌کند که محتوای آن به‌صورت زیر است:

{
"name": "express-api",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "MIT"
}

حال برای نصب Express بایستی دستور زیر را اجرا کنید:

npm install express

و همان‌طور که فایل main برنامه‌ی ما index.js است بایستی یک فایل با همین نام در این مسیر ایجاد کنید:

  • Linux:
touch index.js
  • Widnows:
.> index.js

پس از ایجاد فایل index.js، برای ایجاد یک Express API بایستی کدهای زیر را در آن فایل قرار دهید:

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

// Port
const port = 3000;

app.get("/", (req, res) => {
  res.json({
    message: "Hello Stranger! How are you?",
  });
});

// Listen
app.listen(port, () => {
  console.log(`Listening on port: ${port}`);
});

در مرحله‌ی بعد بایستی با اجرای دستور زیر، ابزار nodemon را به‌عنوان dev dependency پروژه نصب کنید:

npm install -D nodemon

با استفاده از این ابزار دیگر نیازی نیست که هر مرتبه سرور Express.js خود را مجددا راه‌اندازی کنیم زیرا nodemon تغییرهای ایجاد شده را شناسایی کرده و به‌طور خودکار سرور را راه‌اندازی می‌کند.

برای استفاده از این ابزار بایستی بخش "scripts" را در فایل package.json خود تغییر دهید:

"scripts": {
    "start": "node index.js",
    "dev": "nodemon index.js"
  },

درنهایت پس از انجام همه‌ی این کارها، فایل package.json شما به شکل زیر خواهد بود:

{
  "name": "express-api",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "node index.js",
    "dev": "nodemon index.js"
  },
  "keywords": [],
  "author": "",
  "license": "MIT",
  "dependencies": {
    "express": "^4.17.1"
  },
  "devDependencies": {
    "nodemon": "^2.0.6"
  }
}

حال برای راه‌اندازی سرور می‌توانید دستور زیر را اجرا کنید:

npm run dev

که خروجی اجرای دستور فوق، به شکل زیر است:

[nodemon] 2.0.5
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,json
[nodemon] starting `node index.js`
Listening on port: 3000

پس از راه‌اندازی برنامه می‌توانید از طریق آدرس localhost:3000، خروجی برنامه‌ را مشاهده کنید:

{
  "message": "Hello Stranger! How are you?"
}

نحوه‌ی استفاده از Express middlewareهای محبوب

در مرحله‌ی قبل یک Express API ساده را راه‌اندازی کردیم. حال در این بخش قصد داریم تا به بررسی و استفاده از پنج Express middleware محبوب بپردازیم:

۱) Morgan

morgan را می‌توان یک logger middleware دانست که برای تمام درخواست‌هایی که به API ارسال می‌شوند، logهایی را تولید می‌کند. بهترین نقطه‌ی قوت این ابزار در این است که شما می‌توانید یک فرمت از پیش تعریف شده یا یک فرمت جدید را بر اساس نیاز خود تعریف و استفاده کنید.

برای نصب morgan، دستور زیر را اجرا کنید:

npm install morgan

morgan شامل بسیاری فرمت‌های از پیش تعریف شده است که شما می‌توانید از آن‌ها استفاده کنید. بسیاری از توسعه‌دهندگان ترجیح می‌دهند از خروجی استاندارد logهای Apache استفاده کنند.

برای استفاده از morgan، فایل index.js برنامه‌ی خود را به شکل زیر تغییر دهید:

const express = require("express");
const morgan = require("morgan")

const app = express();

// Middlewares
app.use(morgan("common"))


// Port
const port = 3000;

app.get("/", (req, res) => {
  res.json({
    message: "Hello Stranger! How are you?",
  });
});

// Listen
app.listen(port, ()=>{
    console.log(`Listening on port: ${port}`)
})

پس از ایجاد تغییرهای فوق می‌توانید مشاهده کنید که پس از راه‌اندازی مجدد برنامه توسط nodemon، خروجی زیر با ارسال هر درخواست به localhost:3000 نمایش داده می‌شود:

::ffff:127.0.0.1 - - [14/Oct/2020:09:21:16 +0000] "GET / HTTP/1.1" 304 -

قبل از مطالعه‌ی ادامه‌ی مقاله بایستی متذکر شویم که Middlewareها بر اساس ترتیبی که آن‌ها را تعریف می‌کنید، اجرا می‌شوند.

۲) Helmet

Helmet یک Middleware است که به‌منظور تامین امنیت برنامه‌های Express.js با تنظیم Headerهای مختلف استفاده می‌شود. برای درک بهتر نحوه‌ی کار Helmet، وارد آدرس localhost:3000 شوید و سپس کلیدهای ترکیبی CTRL + Shift + J را در مرورگر Chrome یا CTRL + Shift+ K را در مرورگر Firefox فشار دهید و به تب Network بروید.

تب network

اگر تب Network خالی بود، دکمه‌ی F5 را فشار دهید تا صفحه مجددا بارگیری شود.

بارگیری مجدد صفحه‌ی وب برای نمایش اطلاعات در تب network

در تصویر فوق می‌توانید درخواست favicon را نادیده بگیرید زیرا در بخش‌ دیگری به آن می‌پردازیم اما توجه خود را به بخش درخواست‌های GET / و Response Headerها متمرکز کنید.

response headerهای پیش‌فرض برنامه‌های express

ممکن است متوجه آسیب‌پذیری خاصی نشوید اما مهاجمین می‌توانند از تعیین نشدن Headerهای مناسب، برای اجرای حمله‌های خود استفاده کنند. به‌ویژه فیلد X-Powered-By: Express نمایانگر آن است که برنامه‌ی شما توسط Express.js توسعه داده شده و این موضوع خطر را جدی‌تر می‌کند.

Helmet مجموعه‌ای از دوازده Middleware است که از برنامه‌ی شما در برابر آسیب‌پذیری‌ها و حمله‌های شناخته شده، محافظت می‌کند. دستور زیر را برای نصب helmet اجرا کنید:

npm install --save helmet

پس از نصب این ابزار بایستی تغییرهایی را در فایل index.js به‌منظور استفاده از helmet ایجاد کنید:

const express = require("express");
const morgan = require("morgan")
const helmet = require("helmet");
const app = express();


// Middlewares
app.use(morgan("common"))
app.use(helmet());
// Port
const port = 3000;

app.get("/", (req, res) => {
  res.json({
    message: "Hello Stranger! How are you?",
  });
});
// Listen
app.listen(port, () => {
  console.log(`Listening on port: ${port}`);
});

پس از ایجاد این تغییرها، آدرس localhost:3000 را مجددا باز کرده و به تب Network بروید.

تغییرات ایجاد شده توسط helmet در response headerها

همان‌طور که مشاهده می‌کنید، موارد جدیدی در Response Headerها تعریف شده است و همچنین فیلد X-Powered-By: Express پاک شده. علاوه‌براین‌ها می‌توانید helmet() را به‌گونه‌ای پیکربندی کنید که برخی عملکردهای آن غیرفعال شود:

// This disables the `referrerPolicy` middleware but keeps the rest.
app.use(
    helmet({
        referrerPolicy: false,
    })
  );

۳) CORS

CORS مخفف‌ شده‌ی Cross-Origin Resource Sharing است. حال در این بخش به فعال کردن و پیکربندی CORS در برنامه‌های Express.js می‌پردازیم.

تصور کنید که شما یک برنامه‌نویس full-stack هستید و فرانت‌اند برنامه‌ی خود را با React در پورت 3000 راه‌اندازی کرده‌اید و بک‌اند برنامه توسط Express در پورت 8000 راه‌اندازی شده است. با این سناریو درخواست‌ها که از سمت کاربر یا همان فرانت‌اند برنامه‌ به بک‌اند ارسال می‌شود اما اگر توجه کنید، تمام درخواست‌ها با خطا روبرو خواهند شد زیرا درخواست‌ها از یک مبدا متفاوت از سرور Express، به برنامه ارسال شده‌اند.

خطای cors

بنابراین شما بایستی سرور را به‌گونه‌ای پیکربندی کنید که حتی درخواست‌های مبداهای دیگر، پذیرفته شود. دستور زیر را برای نصب cors اجرا کنید:

npm install --save cors

و پس از آن فایل index.js را به شکل زیر تغییر دهید:

const express = require("express");
const morgan = require("morgan")
const helmet = require("helmet");
const cors = require("cors");
const app = express();


// Middlewares
app.use(morgan("common"))
app.use(helmet());
app.use(cors())

// Port
const port = 3000;

app.get("/", (req, res) => {
  res.json({
    message: "Hello Stranger! How are you?",
  });
});
// Listen
app.listen(port, () => {
  console.log(`Listening on port: ${port}`);
});

با اضافه کردن کد app.use(cors()) تمام درخواست‌ها از هر مبدایی پذیرفته می‌شوند که در بعضی موارد این موضوع می‌تواند برنامه‌ی شما را آسیب‌پذیر کند. بیایید مثال قبلی که درباره‌های برنامه‌‌ای با فرانت‌اند React و بک‌اند Express.js بود را مجددا در نظر بگیریم. به‌جای قبول کردن همه‌ی درخواست‌ها از مبداهای مختلف می‌توانیم یک white list از مبداهای مورد اعتماد تهیه کرده و فقط درخواست‌هایی که از آن مبدا‌ها ارسال می‌شوند را پاسخ دهیم:

// whitelist
const whitelist = ['http://localhost:3000', 'http://localhost:3001']
const corsOptions = {
  origin: function (origin, callback) {
    if (whitelist.indexOf(origin) !== -1) {
      callback(null, true)
    } else {
      callback(new Error('Not allowed by CORS'))
    }
  }
}
app.use(cors(corsOptions));
پیام دریافت شده پس از پیکربندی صحیح cors

برای کسب اطلاعات بیشتر در رابطه با CORS می‌توانید به مستندات موجود در سامانه MDN مراجعه کنید.

۴) Express Rate Limit

Express Rate Limit یک Middleware برای محدود کردن تعداد درخواست‌ها در Express.js است. یعنی به کمک این Middleware می‌توانیم تعداد درخواست‌های مکرر از یک آدرس IP را محدود کنیم.

دستور زیر را برای نصب express-rate-limit اجرا کنید:

npm install --save express-rate-limit

برای استفاده از این Middleware بایستی در فایل index.js یک متغیر با نام limiter ایجاد کرده و در ادامه از آن برای پیکربندی express-rate-limit استفاده ‌کنیم:

const rateLimit = require("express-rate-limit");

const limiter = rateLimit({
    windowMs: 15 * 60 * 1000, // 15 minutes
    max: 100 // limit each IP to 100 requests per windowMs
  });

با استفاده از کد فوق، هر آدرس IP به ۱۰۰ درخواست در مدت زمان ۱۵ دقیقه محدود می‌شود.

برای اعمال محدودیت فوق بایستی فایل index.js را به‌صورت زیر به‌روزرسانی کنید:

const express = require("express");
const morgan = require("morgan")
const helmet = require("helmet");
const cors = require("cors");
const rateLimit = require("express-rate-limit");

const limiter = rateLimit({
    windowMs: 15 * 60 * 1000, // 15 minutes
    max: 100 // limit each IP to 100 requests per windowMs
  });

const app = express();


// Middlewares
app.use(morgan("common"))
app.use(helmet());
app.use(cors())
app.use(limiter); //  apply to all requests

// Port
const port = 3000;

app.get("/", (req, res) => {
  res.json({
    message: "Hello Stranger! How are you?",
  });
});
// Listen
app.listen(port, () => {
  console.log(`Listening on port: ${port}`);
});

برای درک بهتر این موضوع می‌توانید محدودیت زمانی و تعداد درخواست‌ها را کمتر کنید:

const limiter = rateLimit({
    windowMs: 60 * 1000, // 1 minute
    max: 2, // limit each IP to 2 requests per windowMs
    message: "Too many accounts created from this IP, please try again after a minute"
  });

حال اگر وارد آدرس localhost:3000 شوید و صفحه‌ها را سه یا چهار مرتبه رفرش کنید، پیغام Too many accounts created from this IP, please try again after a minute به شما نمایش داده می‌شود.

نمایش خطا توسط express rate limit

از آنجا که morgan هنوز کار می‌کند، می‌توانید گزارش‌ها را در Terminal مشاهده کنید:

::1 - - [14/Nov/2020:08:15:58 +0000] "GET / HTTP/1.1" 304 -
::1 - - [14/Nov/2020:08:15:59 +0000] "GET / HTTP/1.1" 304 -
::1 - - [14/Nov/2020:08:15:59 +0000] "GET / HTTP/1.1" 429 71

کد ۴۲۹ نشان می‌دهد که کاربر درخواست‌های زیادی را به‌نسبت مدت زمان مشخص شده‌، ارسال کرده است. علاوه‌براین‌ها شما می‌توانید express-rate-limit را به‌گونه‌ای پیکربندی کنید که فقط درخواست‌ به Routeهای خاصی را محدود کند:

//  apply to all requests
app.use(limiter); 

// only apply to requests that begin with /api/
app.use("/api/", limiter);

همچنین در لیست زیر می‌توانید برخی دیگر از Middlewareهایی که برای محدود کردن تعداد درخواست‌ها استفاده می‌شوند را مشاهده کنید:

۵) serve-favicon

serve-favicon یک Middleware برای serve کردن favicon در Express.js است. احتمالا به‌خاطر دارید که تب Network را در بخش‌های قبلی باز کرده بودیم و یک درخواست ناموفق برای favicon در تب Network وجود داشت.

خطا در درخواست favicon از وب سرور

favicon یک آیکون کوچک است که اغلب در سمت چپ عنوان صفحه در نوار آدرس نمایش داده می‌شود. حال برای نصب serve-favicon بایستی دستور زیر را اجرا کنید:

npm install serve-favicon

همچنین به یک favicon در مسیر پروژه نیاز خواهید داشت. پس از قراردادن favicon در مسیر پروژه می‌توانید فایل index.js را به شکل زیر به‌روزرسانی کنید:

const express = require("express");
const morgan = require("morgan")
const helmet = require("helmet");
const cors = require("cors");
const rateLimit = require("express-rate-limit");
var favicon = require('serve-favicon')


const limiter = rateLimit({
    windowMs: 15 *60 * 1000, // 15 minutes
    max: 100, // limit each IP to 100 requests per windowMs
    message: "Too many accounts created from this IP, please try again after a minute"
  });

const app = express();

// Serve Favicon
app.use(favicon('favicon.ico'))

// Middlewares
app.use(morgan("common"))
app.use(helmet());
app.use(cors())
app.use(limiter); //  apply to all requests

// Port
const port = 3000;

app.get("/", (req, res) => {
  res.json({
    message: "Hello Stranger! How are you?",
  });
});
// Listen
app.listen(port, () => {
  console.log(`Listening on port: ${port}`);
});

اگر favicon شما در پوشه‌ی public قرار داشته باشد می‌توانید از path استفاده کنید:

var path = require('path')
 ...

// Serve Favicon
app.use(favicon(path.join(__dirname, 'public', 'favicon.ico')))
...

حال اگر آدرس localhost:3000 را مجددا بارگیری کنید، favicon انتخابی شما نمایش داده می‌شود.

نمایش favicon

تب Network را باز کرده و صفحه را reload کنید.

درخواست موفقیت‌آمیز favicon

با استفاده از serve-favicon می‌توانید برای بهینه‌سازی عملکرد برنامه‌ی وب خود، favicon را در حافظه‌ی دیسک کاربر، cache کنید. به‌طور پیش‌فرض با استفاده از serve-favicon، آیکون مورد نظر به‌مدت یک سال cache می‌شود:

Cache-Control: public, max-age=31536000

اما برای تغییر این مدت زمان می‌توانید از مقدار maxAge استفاده کنید:

// Serve Favicon
app.use(
  favicon("favicon.ico", {
    maxAge: 500 * 60 * 60 * 24 * 1000,
  })
);

منبع: https://blog.logrocket.com/express-middleware-a-complete-guide