full
This commit is contained in:
83
utils/createRateLimiter.js
Normal file
83
utils/createRateLimiter.js
Normal file
@@ -0,0 +1,83 @@
|
||||
const rateLimit = require('express-rate-limit')
|
||||
const { getRedisClient } = require('./redisClient')
|
||||
|
||||
/**
|
||||
* پیادهسازیِ سبکِ خودمون از Store — بهجای rate-limit-redis. علتش: rate-limit-redis
|
||||
* موقعِ ساختنِ RedisStore (سازنده، همون لحظهی require شدنِ این ماژول، خیلی زودتر از
|
||||
* وصلشدنِ کاملِ Redis) فوراً و بدون await دو تا SCRIPT LOAD میفرسته و نتیجهشون
|
||||
* (Promise) رو *برای همیشهی عمرِ اون instance* کش میکنه. اگه Redis همون لحظهی
|
||||
* boot در دسترس نباشه، اون دو Promise رد میشن (unhandled rejection، حتی میتونه
|
||||
* کل پروسه رو کرش کنه) و از اون به بعد، حتی وقتی Redis بعداً وصل بشه، همون Store
|
||||
* برای همیشه شکسته میمونه چون SHAی اسکریپت هیچوقت دوباره لود نمیشه.
|
||||
*
|
||||
* این پیادهسازی فقط از INCR/PEXPIRE/DECR/DEL استفاده میکنه (بدون اسکریپتِ لوا،
|
||||
* بدون هیچ حالتِ ساختهشدهی یکبارمصرف) — هر متد کاملاً مستقلِ متدهای دیگهست، پس
|
||||
* هیچ حالتی نیست که «یه بار شکست خورد، برای همیشه خرابه»
|
||||
*/
|
||||
class SimpleRedisRateLimitStore {
|
||||
constructor (prefix, windowMs) {
|
||||
this.prefix = prefix
|
||||
this.windowMs = windowMs
|
||||
}
|
||||
|
||||
async increment (key) {
|
||||
const redis = getRedisClient()
|
||||
if (!redis) throw new Error('Redis not ready')
|
||||
const redisKey = this.prefix + key
|
||||
const totalHits = await redis.incr(redisKey)
|
||||
if (totalHits === 1) {
|
||||
// اولین ضربهی این پنجره — شروعکنندهٔ TTL. اگه دقیقاً همینجا پروسه کرش کنه
|
||||
// (قبل از رسیدن به pExpire)، این کلید بدون انقضا میمونه — یه edge-caseِ
|
||||
// شناختهشده و پذیرفتهشده برای این نوع شمارندهی ساده، نه یه شمارندهی
|
||||
// امنیتیای که باید صددرصد atomic باشه
|
||||
await redis.pExpire(redisKey, this.windowMs)
|
||||
}
|
||||
const ttl = await redis.pTTL(redisKey)
|
||||
return {
|
||||
totalHits,
|
||||
resetTime: ttl > 0 ? new Date(Date.now() + ttl) : undefined
|
||||
}
|
||||
}
|
||||
|
||||
async decrement (key) {
|
||||
const redis = getRedisClient()
|
||||
if (!redis) return
|
||||
await redis.decr(this.prefix + key)
|
||||
}
|
||||
|
||||
async resetKey (key) {
|
||||
const redis = getRedisClient()
|
||||
if (!redis) return
|
||||
await redis.del(this.prefix + key)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* میسازه: یه میدلورِ ریتلیمیت که خودش روی هر درخواست بین Redis (وقتی وصله —
|
||||
* محدودیتِ سراسری بین همهی اینستنسهای سرور، برای مقیاسپذیری/امنیتِ واقعی لازمه —
|
||||
* مثلاً برای جلوگیری از brute-force روی لاگین وقتی چند سرور پشتِ لودبالانسر باشه)
|
||||
* و حافظهی in-process (fallback وقتی Redis در دسترس نیست — فقط per-instance، ولی
|
||||
* از کرشکردن یا بیاثرشدنِ کاملِ محدودیت بهتره) سوییچ میکنه.
|
||||
*
|
||||
* چون express-rate-limit اجازه نمیده store رو بعد از ساختنِ میدلور عوض کنی، دو
|
||||
* نمونهی کاملاً جدا ساخته میشه و روی هر درخواست، بر اساسِ آمادهبودنِ فعلیِ Redis،
|
||||
* یکیشون صدا زده میشه.
|
||||
*
|
||||
* @param {string} prefix - پیشوندِ یکتا برای کلیدهای این limiter توی Redis (تا با
|
||||
* limiterهای دیگه قاطی نشن)
|
||||
* @param {import('express-rate-limit').Options} options
|
||||
*/
|
||||
function createRateLimiter (prefix, options) {
|
||||
const memoryLimiter = rateLimit(options)
|
||||
const redisLimiter = rateLimit({
|
||||
...options,
|
||||
store: new SimpleRedisRateLimitStore(`rl:${prefix}:`, options.windowMs)
|
||||
})
|
||||
|
||||
return (req, res, next) => {
|
||||
const limiter = getRedisClient() ? redisLimiter : memoryLimiter
|
||||
limiter(req, res, next)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = createRateLimiter
|
||||
Reference in New Issue
Block a user