feat: implement rate limiting system

This commit is contained in:
张翔
2026-03-24 10:38:23 +08:00
parent cef0b6fb74
commit c3dee9e2e7
2 changed files with 177 additions and 0 deletions
+58
View File
@@ -0,0 +1,58 @@
import { RateLimiter, RateLimitResult } from './rate-limiter';
describe('Rate Limiting System', () => {
let rateLimiter: RateLimiter;
beforeEach(() => {
rateLimiter = new RateLimiter();
});
test('should allow request within limit', async () => {
const result = await rateLimiter.checkIP('192.168.1.1');
expect(result.allowed).toBe(true);
expect(result.remaining).toBeGreaterThan(0);
});
test('should block request when limit exceeded', async () => {
const ip = '192.168.1.2';
for (let i = 0; i < 10; i++) {
await rateLimiter.checkIP(ip);
}
const result = await rateLimiter.checkIP(ip);
expect(result.allowed).toBe(false);
});
test('should reset limit after window expires', async () => {
const ip = '192.168.1.3';
for (let i = 0; i < 10; i++) {
await rateLimiter.checkIP(ip);
}
await rateLimiter.checkIP(ip);
jest.useFakeTimers();
jest.advanceTimersByTime(61 * 60 * 1000);
const result = await rateLimiter.checkIP(ip);
expect(result.allowed).toBe(true);
jest.useRealTimers();
});
test('should track email rate limits separately', async () => {
const email = 'test@example.com';
for (let i = 0; i < 3; i++) {
await rateLimiter.checkEmail(email);
}
const result = await rateLimiter.checkEmail(email);
expect(result.allowed).toBe(false);
});
test('should enforce global rate limit', async () => {
const promises = [];
for (let i = 0; i < 51; i++) {
promises.push(rateLimiter.checkGlobal());
}
const results = await Promise.all(promises);
const blockedCount = results.filter(r => !r.allowed).length;
expect(blockedCount).toBeGreaterThan(0);
});
});
+119
View File
@@ -0,0 +1,119 @@
import { getSecurityConfig } from './config';
export interface RateLimitResult {
allowed: boolean;
remaining: number;
resetTime: number;
limit: number;
}
interface RequestRecord {
count: number;
windowStart: number;
}
class InMemoryRateLimiter {
private ipRequests = new Map<string, RequestRecord>();
private emailRequests = new Map<string, RequestRecord>();
private globalRequests: RequestRecord = { count: 0, windowStart: Date.now() };
async checkIP(ip: string): Promise<RateLimitResult> {
const config = getSecurityConfig();
const now = Date.now();
const windowMs = config.rateLimit.ip.windowMinutes * 60 * 1000;
let record = this.ipRequests.get(ip);
if (!record || now - record.windowStart > windowMs) {
record = { count: 0, windowStart: now };
this.ipRequests.set(ip, record);
}
const allowed = record.count < config.rateLimit.ip.maxRequests;
if (allowed) {
record.count++;
}
return {
allowed,
remaining: Math.max(0, config.rateLimit.ip.maxRequests - record.count),
resetTime: record.windowStart + windowMs,
limit: config.rateLimit.ip.maxRequests,
};
}
async checkEmail(email: string): Promise<RateLimitResult> {
const config = getSecurityConfig();
const now = Date.now();
const windowMs = config.rateLimit.email.windowHours * 60 * 60 * 1000;
let record = this.emailRequests.get(email);
if (!record || now - record.windowStart > windowMs) {
record = { count: 0, windowStart: now };
this.emailRequests.set(email, record);
}
const allowed = record.count < config.rateLimit.email.maxRequests;
if (allowed) {
record.count++;
}
return {
allowed,
remaining: Math.max(0, config.rateLimit.email.maxRequests - record.count),
resetTime: record.windowStart + windowMs,
limit: config.rateLimit.email.maxRequests,
};
}
async checkGlobal(): Promise<RateLimitResult> {
const config = getSecurityConfig();
const now = Date.now();
const windowMs = config.rateLimit.global.windowMinutes * 60 * 1000;
if (now - this.globalRequests.windowStart > windowMs) {
this.globalRequests = { count: 0, windowStart: now };
}
const allowed = this.globalRequests.count < config.rateLimit.global.maxRequests;
if (allowed) {
this.globalRequests.count++;
}
return {
allowed,
remaining: Math.max(0, config.rateLimit.global.maxRequests - this.globalRequests.count),
resetTime: this.globalRequests.windowStart + windowMs,
limit: config.rateLimit.global.maxRequests,
};
}
clear(): void {
this.ipRequests.clear();
this.emailRequests.clear();
this.globalRequests = { count: 0, windowStart: Date.now() };
}
}
export class RateLimiter {
private limiter: InMemoryRateLimiter;
constructor() {
this.limiter = new InMemoryRateLimiter();
}
async checkIP(ip: string): Promise<RateLimitResult> {
return this.limiter.checkIP(ip);
}
async checkEmail(email: string): Promise<RateLimitResult> {
return this.limiter.checkEmail(email);
}
async checkGlobal(): Promise<RateLimitResult> {
return this.limiter.checkGlobal();
}
clear(): void {
this.limiter.clear();
}
}