我想知道我们是否可以为相同的控制器功能(路由)分别实现 IP 和请求主体(相同的一些用户名)的速率限制(如果满足,任何一个都应该给我错误 429)。
尝试使用以下 packages -
"nestjs-throttler-storage-redis": "^0.3.0"
"@nestjs/throttler": "^4.0.0",
"ioredis": "^5.3.2"
app.module.ts -
ThrottlerModule.forRoot({
ttl: process.env.IP_VELOCITY_TTL as unknown as number, // 24 hours in seconds
limit: process.env.IP_VELOCITY_COUNT as unknown as number, // X number requests per ttl per key (IP address in this case)
storage: new ThrottlerStorageRedisService(new Redis()),
}),
在各自的Module.ts-
{
provide: APP_GUARD,
useClass: ThrottlerGuard,
},
controller.ts -
@Throttle(3, 60 * 60)
但这还不够,因为这会阻止所有请求发布 3 次!
任何人都可以建议我以正确的方式实现这一目标吗?
你需要创建你自己的守卫
extends ThrottlerGuard
并覆盖 getTracker
方法 以便它返回这个 ip
和 req.body.username
组合。有点像
@Injectable()
export class ThrottleIpBodyGuard extends ThrottlerGuard {
getTracker(req: Request) {
return req.ip + req.body.username;
}
}
然后,您可以使用
useClass: ThrottlerGuard
而不是
useClass: ThrottleIpBodyGuard
诀窍是覆盖
ThrottlerGuard
类如下-
import { ExecutionContext, Injectable } from '@nestjs/common';
import { ThrottlerGuard } from '@nestjs/throttler';
@Injectable()
export class CustomThrottlerGuard extends ThrottlerGuard {
// Overwritten to handle the IP restriction along with firstName + lastName restriction
async handleRequest(
context: ExecutionContext,
limit: number,
ttl: number
): Promise<boolean> {
const { req, res } = this.getRequestResponse(context);
// Return early if the current user agent should be ignored.
if (Array.isArray(this.options.ignoreUserAgents)) {
for (const pattern of this.options.ignoreUserAgents) {
if (pattern.test(req.headers['user-agent'])) {
return true;
}
}
}
// Tracker for IP
const tracker = this.getTracker(req);
const key = this.generateKey(context, tracker);
const { totalHits, timeToExpire } = await this.storageService.increment(
key,
ttl
);
// Tracker for firstName and lastName
const firstNameAndLastNameTracker = this.getNameTracker(req);
const nameKey = this.generateKey(context, firstNameAndLastNameTracker);
const { totalHits: totalHitsName, timeToExpire: timeToExpireName } =
await this.storageService.increment(nameKey, ttl);
// Throw an error when the user reached their limit (IP).
if (totalHits > limit) {
res.header('Retry-After', timeToExpire);
this.throwThrottlingException(context);
}
// Throw an Error when user reached their firstName + lastName Limit.
if (
totalHitsName > parseInt(process.env.FIRSTNAME_LASTNAME_MAX_TRY_COUNT)
) {
res.header('Retry-After', timeToExpireName);
this.throwThrottlingException(context);
}
res.header(`${this.headerPrefix}-Limit`, limit);
// We're about to add a record so we need to take that into account here.
// Otherwise the header says we have a request left when there are none.
res.header(
`${this.headerPrefix}-Remaining`,
Math.max(0, limit - totalHits)
);
res.header(`${this.headerPrefix}-Reset`, timeToExpire);
return true;
}
protected getNameTracker(req: Record<string, any>): string {
return req.body.firstName + req.body.lastName;
}
}