各位朋友们好,很久没记录与NestJs相关的文章了。
今天来记录下用nestJs添加搜索历史记录或者访问量,这两个业务需求都有一定的防重复特征,防止同一时间内防重复请求和间隔一段时间允许请求,除了对应的api和间隔时间比较,还可以从用户token、请求IP、请求头自定义参数、Post请求的body、Get请求的query/param等这些方式去进行更加严谨的比较。
当然,我还是建议从用户token、请求IP、请求头自定义参数这三个去比较,它们三个比较不容易那么篡改。
这两个业务需求在我的经验里有三个方案去解决:
redis处理
手写中间件拦截器处理
用@nestjs/throttler包处理
那么今天来记录下第二点和第三点的相关逻辑及代码,相关的nest版本和throttler版本如下:
备注:不同的throttler版本对应不同的nest版本,需要自行去依赖包里查看。
@nestjs/cli: 10.0.3
@nestjs/throttler: 5.2.0
一、手写中间件拦截器处理
先在项目公共的文件夹里加一个中间件的方法,我这里取名叫request-throttle.middleware.ts文件
含有逻辑注释的代码,如下:
import { Injectable, NestMiddleware, HttpStatus } from '@nestjs/common';
@Injectable()
export class LimitInterceptor implements NestMiddleware {
// 请求时间戳的map
private requestTimestamps: Map<string, number> = new Map();
use(req: any, res: any, next: () => void) {
// 获取请求ip
const clientIP = req.ip as string || req.headers['x-forwarded-for'] as string || req.socket.remoteAddress as string;
// 获取前端请求头里传过来的time
let headersTimer = req.headers['request-timer'] ? parseInt(req.headers['request-timer']) : 10000;
// timer最小5000s,小于的时候默认10秒,防止headers里的time遭到篡改。
const timer = headersTimer >= 5000 ? headersTimer : 10000; // 最小从header中获取得时间是5s,防止在headers里篡改
const currentTime = Date.now();
if (this.requestTimestamps.has(clientIP)) {
// 含有上次请求ip,有的话进行时间戳比较
const elapsedTime = currentTime - this.requestTimestamps.get(clientIP)!;
const throttleDuration = timer// 十秒限制
if (elapsedTime < throttleDuration) {
// 相差小于节流时间则返回429状态码和异常
res.status(HttpStatus.TOO_MANY_REQUESTS).send({ code: 200, data: null, message: '请求过于频繁,请稍后再试' });
} else {
// 相差大于节流时间则再次赋值到requestTimestamps里
this.requestTimestamps.set(clientIP, currentTime);
next();
}
} else {
// 无上次请求ip,无的话赋值到requestTimestamps里
this.requestTimestamps.set(clientIP, currentTime);
next();
}
}
}
controller层的代码如下:
import { LimitInterceptor } from '../common/apiRepetitionTime/request-throttle.middleware'
/** 创建搜索历史记录 */
@Post('search/history/create')
@ApiOperation({ summary: '创建搜索历史记录' })
@UseInterceptors(LimitInterceptor) // 拦截器拦截
async createSearchHistory(@Body() createSearchHistoryDto: CreateSearchHistoryDto, @Ip() ip): Promise<ResultData> {
return await this.homeService.createSearchHistory(createSearchHistoryDto, ip)
}
module的代码如下:
import { LimitInterceptor } from '../common/apiRepetitionTime/request-throttle.middleware'
export class ClientWebModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(LimitInterceptor)
.forRoutes({ path: 'clientWeb/search/history/create', method: RequestMethod.POST });
}
}
在上面的代码中,很容易看出,防重复比较和我刚才提的两个业务需求是有点偏差的,搜索历史记录需要比较关键词、不是游客要比较token,还有访问量,需要比较页面链接、用户行为、不是游客也要比较token等,这些都可以自行添加相关比较。
我项目的业务是防止同一个ip某个节流时间内,多次搜索一样的关键词和多次添加搜索历史记录,比较关键词的逻辑处理已经交给搜索的分页查询那个接口处理了。你可能要问为啥要交给搜索的分页查询那个接口处理,因为添加搜索历史记录是需要在用户搜索成功后,前端才去请求添加搜索历史记录的接口,搜索的分页查询那个接口请求失败则不添加用搜索历史记录。
让我们看看效果,(PS:本来想上传视频的,结果掘金文章要西瓜的视频链接才可以)
headers 里传节流时间
搜索关键词
节流时间外请求正常
节流时间内请求异常
这里注意的是,浏览器status code返回的是429状态码,我这里写的code码是200,有点不严谨,可自行改为429状态码。
二、用@nestjs/throttler包处理
@nestjs/throttler包呢它不仅能处理api的请求,也能处理websocket的请求,这个具体是要看文档,当然,它也可以数组多范围内形式,例如范围:5秒内1次、10秒内3次、20秒内8次等,形成数组赋值,满足业务多形态
数组代码如下:
ThrottlerModule.forRoot([
{
name: 'short', // 短期1秒3次
ttl: 1000,
limit: 3,
},
{
name: 'medium', // 中期10秒20次
ttl: 10000,
limit: 20
},
{
name: 'long', // 长期1分钟100次
ttl: 60000,
limit: 100
}
])
controller代码如下:
import { Controller, Get, Query, Body, Post, Req, Param, Put, Ip, UseInterceptors, UseGuards } from '@nestjs/common'
import { ApiTags, ApiOperation, ApiQuery } from '@nestjs/swagger'
import { ThrottlerGuard, Throttle } from '@nestjs/throttler'
/** 查询标签所有列表 */
@Get('tags/AllList')
@ApiOperation({ summary: '查询标签所有列表' })
@UseGuards(ThrottlerGuard)
@Throttle({ default: { limit: 1, ttl: 10000 }}) // 限制为10秒1次
@ApiResult(ContentEntity, true, true)
async findTagsAllList(): Promise<ResultData> {
return await this.contentService.findTagsAllList()
}
module代码如下:
import { ThrottlerModule } from '@nestjs/throttler'
@Module({
imports: [
// api防重复模块
ThrottlerModule.forRoot([
{
ttl: 60000, // 默认缓存时间(毫秒),这里是1分钟
limit: 20, // 默认限制次数
}
])
]
})
具体参数如下:
name | the name for internal tracking of which throttler set is being used. Defaults to default if not passed |
|---|---|
ttl | the number of milliseconds that each request will last in storage |
limit | the maximum number of requests within the TTL limit |
ignoreUserAgents | an array of regular expressions of user-agents to ignore when it comes to throttling requests |
skipIf | a function that takes in the ExecutionContext and returns a boolean to short circuit the throttler logic. Like @SkipThrottler(), but based on the request |
getTracker | a function that takes in the Request and returns a string to override the default logic of the getTracker method |
generateKey | a function that takes in the ExecutionContext, the tacker string and the throttler name as a string and returns a string to override the final key which will be used to store the rate limit value. This overrides the default logic of the generateKey method |
不过它的比较是对比上一次的请求,而不是对比请求成功通行的那次,例如请求第一次成功了,第二次第三次是失败的,第三次对比的是第二次,而不是第一次请求成功了的,也就是说第三次要大于第二次的节流时间,路由守卫才能通行。
讲了这两个业务,当然,还有post、put、delete请求方式的全局路由守卫防重复等业务也可以去处理。
各位朋友们,你们觉得哪种方法比较好呢?
本人开源的nestJs项目,欢迎各位朋友点个start!谢谢!
git链接:gitee.com/wx375149069…