[{"data":1,"prerenderedAt":195},["ShallowReactive",2],{"content:\u002Fposts\u002F2025\u002Fnest-captcha":3,"surround:\u002Fposts\u002F2025\u002Fnest-captcha":185},{"id":4,"title":5,"body":6,"canonical":159,"categories":160,"date":162,"description":163,"draft":164,"extension":165,"image":166,"meta":167,"navigation":168,"path":169,"permalink":159,"readingTime":170,"recommend":159,"references":159,"seo":175,"sitemap":176,"stem":177,"tags":178,"type":159,"updated":162,"__hash__":184},"content\u002Fposts\u002F2025\u002Fnest-captcha.md","在 Nestjs 中使用验证码",{"type":7,"value":8,"toc":142},"minimark",[9,13,24,28,34,42,52,60,70,92,98,106,112,115,119,126,129,136,139],[10,11,12],"h2",{"id":12},"依赖安装",[14,15,21],"pre",{"className":16,"code":18,"language":19,"meta":20},[17],"language-shell","pnpm i svg-captcha uuid @nestjs\u002Fcache-manager\n","shell","",[22,23,18],"code",{"__ignoreMap":20},[10,25,27],{"id":26},"创建并配置-captcha-模块","创建并配置 captcha 模块",[14,29,32],{"className":30,"code":31,"language":19,"meta":20},[17],"nest g res system\u002Fcaptcha --no-spec\n",[22,33,31],{"__ignoreMap":20},[35,36,38,39],"h3",{"id":37},"更改-captchacontrollerts","更改 ",[22,40,41],{"code":41},"captcha.controller.ts",[43,44,45,49],"ol",{},[46,47,48],"li",{},"我们可以选择生成字母数字类型的或者数学计算的验证码",[46,50,51],{},"我们需要一个校验用户是否输入正确验证码的接口",[14,53,58],{"className":54,"code":56,"filename":41,"language":57,"meta":20},[55],"language-ts","import { Controller, Get, Query } from '@nestjs\u002Fcommon';\nimport { ApiTags } from '@nestjs\u002Fswagger';\n\nimport { CaptchaService } from '.\u002Fcaptcha.service';\n\n@ApiTags('本地验证码')\n@Controller('captcha')\nexport class CaptchaController {\n    constructor(private readonly captchaService: CaptchaService) {}\n\n    @Get('svg')\n    createSvg() {\n        return this.captchaService.create('svg');\n    }\n\n    @Get('math')\n    createMath() {\n        return this.captchaService.create('math');\n    }\n\n    @Get('verify')\n    get(@Query('key') key: string, @Query('text') text: string) {\n        return this.captchaService.verify(key, text);\n    }\n}\n","ts",[22,59,56],{"__ignoreMap":20},[35,61,63,64,66,67],{"id":62},"根据-captchacontrollerts-需要的功能我们来更改-captchaservicets","根据 ",[22,65,41],{"code":41}," 需要的功能我们来更改 ",[22,68,69],{"code":69},"captcha.service.ts",[71,72,73,76,83],"ul",{},[46,74,75],{},"这里使用 uuid 可以最大概率保证 key 的唯一性",[46,77,78,79,82],{},"记得 Cache 模块一定要从 ",[22,80,81],{"code":81},"@nestjs\u002Fcache-manager"," 包中导入",[46,84,85,86,91],{},"这里需要用到缓存模块，",[87,88,90],"a",{"href":89},"#%E9%85%8D%E7%BD%AE%E7%BC%93%E5%AD%98%E6%A8%A1%E5%9D%97","下一部分","会说明",[14,93,96],{"className":94,"code":95,"filename":69,"language":57,"meta":20},[55],"import { Cache } from '@nestjs\u002Fcache-manager';\nimport { BadRequestException, Injectable } from '@nestjs\u002Fcommon';\nimport * as svgCaptcha from 'svg-captcha';\nimport { v4 as uuidv4 } from 'uuid';\n\nimport { CacheKeys } from '@\u002Fcommon\u002Fenum\u002Fcache-keys.enum';\n\n@Injectable()\nexport class CaptchaService {\n    constructor(private readonly cache: Cache) {}\n\n    \u002F\u002F 生成验证码，可以选择是否是数学计算\n    async create(type: 'math' | 'svg' = 'svg') {\n        let captcha: svgCaptcha.CaptchaObj;\n        if (type === 'math') {\n            captcha = svgCaptcha.createMathExpr({\n                mathMin: 0,\n                mathMax: 9,\n                noise: 1,\n                color: true,\n            });\n        }\n        else {\n            captcha = svgCaptcha.create({\n                ignoreChars: '0o1i',\n                noise: 1,\n                color: true,\n            });\n        }\n\n        \u002F\u002F 生成一个uuid，调用接口时需要把这个 key 和内容一起传过来，用来校验此次输入是否正确\n        const key = uuidv4();\n        try {\n            \u002F\u002F 将生成好的验证码的正确文本缓存起来，后续用来根据 key 做比对\n            await this.cache.set(`${CacheKeys.CAPTCHA_CACHE_KEY}:${key}`, captcha.text, 60 * 1000); \u002F\u002F 缓存 1 分钟\n        }\n        catch (error) {\n            console.log(error, 'error');\n        }\n\n        return {\n            key,\n            svg: captcha.data,\n        };\n    }\n\n    \u002F\u002F 校验输入的验证码是否正确\n    async verify(key: string, text: string) {\n        const captchaText = await this.cache.get\u003Cstring>(`${CacheKeys.CAPTCHA_CACHE_KEY}:${key}`);\n\n        if (captchaText !== text) {\n            throw new BadRequestException('验证码错误');\n        }\n\n        return true;\n    }\n}\n",[22,97,95],{"__ignoreMap":20},[99,100,102,105],"h4",{"id":101},"cache-keysenumts-的内容为",[22,103,104],{"code":104},"cache-keys.enum.ts"," 的内容为",[14,107,110],{"className":108,"code":109,"filename":104,"language":57,"meta":20},[55],"export const CacheKeys = {\n    CAPTCHA_CACHE_KEY: 'captcha',\n} as const;\n",[22,111,109],{"__ignoreMap":20},[10,113,114],{"id":114},"配置缓存模块",[116,117,118],"p",{},"在 app.module 中我们需要配置一下缓存模块，让它使用 redis 而不是默认的内存缓存",[14,120,124],{"className":121,"code":122,"filename":123,"language":57,"meta":20},[55],"import { createKeyv } from '@keyv\u002Fredis';\nimport { CacheModule } from '@nestjs\u002Fcache-manager';\nimport { Module } from '@nestjs\u002Fcommon';\n\nimport { ConfigModule } from '@\u002Fconfig\u002Fconfig.module';\nimport { ConfigService } from '@\u002Fconfig\u002Fconfig.service';\n\nimport { AppController } from '.\u002Fapp.controller';\nimport { CaptchaModule } from '.\u002Fsystem\u002Fcaptcha\u002Fcaptcha.module';\n\n@Module({\n    imports: [\n        CacheModule.registerAsync({\n            imports: [ConfigModule],\n            inject: [ConfigService],\n            isGlobal: true, \u002F\u002F 这里有 global 后，别的模块使用时直接 `private readonly cache: Cache` 注入即可\n            useFactory(config: ConfigService) {\n                return {\n                    stores: [\n                        createKeyv({\n                            url: `redis:\u002F\u002F${config.cache.redis.host}:${config.cache.redis.port}`,\n                            password: config.cache.redis.password,\n                        }),\n                    ],\n                    ttl: config.cache.ttl,\n                };\n            },\n        }),\n        CaptchaModule,\n    ],\n    controllers: [AppController],\n    providers: [AppService],\n})\nexport class AppModule {}\n","app.module.ts",[22,125,122],{"__ignoreMap":20},[116,127,128],{},"这里的 config 模块使用我们自己改变的一种写法（当然这个是在官方文档里有说明的），这种写法可以让我们在取用配置时有提示，我这里有另一篇文章来说明",[130,131],"link-banner",{"banner":132,"description":133,"link":134,"title":135},"https:\u002F\u002Fbitmc.uno\u002Fpicgo\u002Fnest-config.jpg","nestjs 官方的 nest-config 模块已经很好用了，但是我们有更好用的方法！","nest-config","nest-config 模块的人体工学适配",[10,137,138],{"id":138},"清理代码",[116,140,141],{},"使用命令创建的 captcha 模块会有 entity 和 dto，这里我们用不到可以将其删除。",{"title":20,"searchDepth":143,"depth":143,"links":144},4,[145,147,157,158],{"id":12,"depth":146,"text":12},2,{"id":26,"depth":146,"text":27,"children":148},[149,152],{"id":37,"depth":150,"text":151},3,"更改 captcha.controller.ts",{"id":62,"depth":150,"text":153,"children":154},"根据 captcha.controller.ts 需要的功能我们来更改 captcha.service.ts",[155],{"id":101,"depth":143,"text":156},"cache-keys.enum.ts 的内容为",{"id":114,"depth":146,"text":114},{"id":138,"depth":146,"text":138},null,[161],"代码","2025-08-10 22:53:48","在登录时，我们需要提供一个验证码来防止攻击，这篇文章就来说明一下如何实现验证码模块。",false,"md","https:\u002F\u002Fbitmc.uno\u002Fpicgo\u002Fnest-captcha.jpg",{},true,"\u002Fposts\u002F2025\u002Fnest-captcha",{"text":171,"minutes":172,"time":173,"words":174},"4 min read",3.41,204600,682,{"title":5,"description":163},{"loc":169},"posts\u002F2025\u002Fnest-captcha",[179,180,181,182,183],"nest","captcha","验证码","cache","redis","cCuAtXQ31HC4sAyqkVxM8H_vtEe_bxstfJoJBIkoCT0",[186,191],{"title":187,"path":188,"stem":189,"date":190,"type":159,"children":-1},"在 Nestjs 中使用 Drizzle ORM","\u002Fposts\u002F2025\u002Fnestjs-drizzle","posts\u002F2025\u002Fnestjs-drizzle","2025-08-06 23:01:32",{"title":135,"path":192,"stem":193,"date":194,"type":159,"children":-1},"\u002Fposts\u002F2025\u002Fnest-config","posts\u002F2025\u002Fnest-config","2025-08-11 00:07:11",1789642588891]