【NestJs】记录用nest添加搜索历史记录或者页面访问量

344 阅读5分钟

各位朋友们好,很久没记录与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文件

9992.png

含有逻辑注释的代码,如下:

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 里传节流时间 222.png

搜索关键词 333.png

节流时间外请求正常 444.png

节流时间内请求异常 555.png

这里注意的是,浏览器status code返回的是429状态码,我这里写的code码是200,有点不严谨,可自行改为429状态码。

二、用@nestjs/throttler包处理

@nestjs/throttler包呢它不仅能处理api的请求,也能处理websocket的请求,这个具体是要看文档,当然,它也可以数组多范围内形式,例如范围:5秒内1次、10秒内3次、20秒内8次等,形成数组赋值,满足业务多形态

数组代码如下:

    ThrottlerModule.forRoot([
      {
        name: 'short', // 短期13ttl: 1000,
        limit: 3,
      },
      {
        name: 'medium', // 中期1020ttl: 10000,
        limit: 20
      },
      {
        name: 'long', // 长期1分钟100ttl: 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, // 默认限制次数
      }
    ])
  ]
})

具体参数如下:

namethe name for internal tracking of which throttler set is being used. Defaults to default if not passed
ttlthe number of milliseconds that each request will last in storage
limitthe maximum number of requests within the TTL limit
ignoreUserAgentsan array of regular expressions of user-agents to ignore when it comes to throttling requests
skipIfa function that takes in the ExecutionContext and returns a boolean to short circuit the throttler logic. Like @SkipThrottler(), but based on the request
getTrackera function that takes in the Request and returns a string to override the default logic of the getTracker method
generateKeya 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…