您好,欢迎来到个人技术集锦。
搜索
当前位置:首页nest实现前端图形校验

nest实现前端图形校验

个人技术集锦 2025-06-08
导读在实战开发中,我们经常用到图形校验,具体怎么实现的呢? 安装模块 需要用到的模块有 svg-captcha #图形模块 uuid #随机id模块 redis #redis缓存数据 #最后还有 class-transformer class-validator #两个格式校验pipe模块 链接redis 我们把生成的图形答案需要放到redis中,这样的话,可以保证验证码的安全性和跨进程共享的特点 我们先创建一个nest的项目 使用nest g mo redis --no-spec 创建一个r

在实战开发中,我们经常用到图形校验,具体怎么实现的呢?

安装模块

需要用到的模块有

svg-captcha #图形模块
uuid #随机id模块
redis #redis缓存数据
#最后还有
class-transformer class-validator #两个格式校验pipe模块

链接redis

我们把生成的图形答案需要放到redis中,这样的话,可以保证验证码的安全性和跨进程共享的特点

我们先创建一个nest的项目

使用nest g mo redis --no-spec 创建一个redis.module.ts [用来链接redis的配置]

创建一个redis.constants.ts [用来编写redis的签名,就是类似于enum的作用]

使用nest g s redis --no-spec 创建一个redis.service.ts [用来封装我们的redis的操作,但此项目中,并没有使用] 可以忽略

我们安装了redis 模块, 就可以使用动态模块的方式注入redis的链接配置了

import { Global, Module } from '@nestjs/common';
import { RedisService } from './redis.service';
import { createClient } from 'redis';

const createRedisClient = async () => {
  const client = createClient({
    socket: {
      host: '127.0.0.1',
      port: 6379,
    },
  });
  await client.connect();
  return client;
};

@Global()  // 设置为全局模块,其他模块无需重复引入
@Module({
  providers: [
    {
      provide: 'REDIS_CLIENT',
      useFactory: createRedisClient,
    }
    , RedisService],
  exports: [RedisService, 'REDIS_CLIENT'],
})
export class RedisModule {
}

这里的话,是我们的constants签名文件 ,一些内容,我不方便直接硬编码,和TS的enum一样的其实

export const REDIS_CAPTCHA_PREFIX = 'captcha:';
export const REDIS_USER_SESSION_PREFIX = 'session:';
export const REDIS_SMS_CODE_PREFIX = 'sms:';

生成校验 svg

然后我们现在来书写生成图片和校验图片的代码

这里的generateCaptcha 方法使用来生成图片的

它主要做了什么?

还有一个validateCaptcha方法

它做了什么?

  1. 从前端的请求头里拿到我们刚刚生成图形时候给的结合版id
  2. 拿到我们的input 的图形码
  3. 然后拿着id去找redis里的缓存进行对比
  4. 正确了,就返回 true
  5. 失败了,就删除我们的key,然后等它分钟,redis里的会自己删除的
import { Inject, Injectable } from '@nestjs/common';
import * as svgCaptcha from 'svg-captcha';  //图片生成
import { v4 as uuidv4 } from 'uuid'; //uuid
import { REDIS_CAPTCHA_PREFIX } from '../redis/redis.constants';  //签名
import { RedisClientType } from 'redis'; //使用redis

@Injectable()
export class CaptchaService {

  // 注入redis
  @Inject('REDIS_CLIENT')
  private redisClient: RedisClientType;

  // 生成图片方法
  async generateCaptcha() {

    // 生成图片
    const captcha = svgCaptcha.create({
      size: 4, // 验证码长度
      noise: 2, // 干扰线
      color: true, // 彩色文字
      background: '#cc9966',
      width: 120,
      height: 40,
    });

    // 随机生成一个uuid
    const captchaId = uuidv4();

    // 使用uuid结合签名生成一个key,用来保存到redis
    const key = `${REDIS_CAPTCHA_PREFIX}${captchaId}`;

    // 保存到redis
    // set(key,value,options)
    await this.redisClient.set(key, captcha.text, {
      // 过期时间等配置
      expiration: {
        type: 'EX',
        value: 300,
      },
    });


    return {
      // captcha,
      captchaId,
      svg: captcha.data,
    };

  }

  // 校验方法
  // 从前端的header中拿到id
  async validateCaptcha(captchaId: string, userInput: string): Promise<boolean> {
    const key = `${REDIS_CAPTCHA_PREFIX}${captchaId}`;
    const value = await this.redisClient.get(key);

    if (!value) return false;

    // 删除验证码(防止复用)
    await this.redisClient.del(key);

    return value.toLowerCase() === userInput.toLowerCase();
  }

}

前端路由 

这里做的主要就是图形生成请求和校验请求处理

  • 我们请求图形后,我们给配置它的header信息,保存我们的结合id
  • 校验的时候,就是我说的,从header里拿到id 和我们input的value和redis里的对比即可
import { Body, Controller, Get, HttpException, HttpStatus, Post, Res } from '@nestjs/common';
import { CaptchaService } from './captcha.service';
import { Response } from 'express';
import { ValidateCaptchaDto } from './dto/validate-captcha.dto';


@Controller('captcha')
export class CaptchaController {
  constructor(private readonly captchaService: CaptchaService) {
  }


  @Get()
  async getCaptcha(@Res() res: Response) {

    // 生成,拿到两个东西
    const { captchaId, svg } = await this.captchaService.generateCaptcha();


    res.setHeader('Content-Type', 'image/svg+xml');
    res.setHeader('Captcha-Id', captchaId); // 把 captchaId 放到响应头里
    res.send(svg);  // ✅ 把 svg 图片直接作为返回体
  }


  // POST /captcha/verify
  @Post('verify')
  async verifyCaptcha(@Body() dto: ValidateCaptchaDto) {
    const isValid = await this.captchaService.validateCaptcha(dto.captchaId, dto.code);
    if (!isValid) {
      throw new HttpException('验证码错误或已过期', HttpStatus.BAD_REQUEST);
    }

    return {
      message: '验证码通过',
    };
  }

}

 dto

我们请求时的dto对象

import { IsNotEmpty } from 'class-validator';

export class ValidateCaptchaDto {
  @IsNotEmpty({ message: 'captchaId 不能为空' })
  captchaId: string;

  @IsNotEmpty({ message: '验证码不能为空' })
  code: string;
}

前端测试

main

main.js里要两个配置

一个是使用我们的校验包

一个是开启跨域

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';


async function bootstrap() {
  const app = await NestFactory.create(AppModule);


  app.useGlobalPipes(new ValidationPipe({
    whitelist: true,
    forbidNonWhitelisted: true,
    transform: true,
  }));


  app.enableCors({
    origin: true, // 或指定为 'http://localhost:5500' 之类你的 HTML 页面地址
    credentials: true,
    exposedHeaders: ['Captcha-Id'], // 非常关键!暴露自定义响应头
  });
  await app.listen(process.env.PORT ?? 3000);
}

bootstrap();

index.html

一上来,先发一个请求,获取图形,并获取我们的节点,给他添加src属性

然后分别写了两个请求

这里逻辑很简单,自己看看吧

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
  <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
  <style>
      body {
          font-family: sans-serif;
          padding: 20px;
      }

      .captcha-box {
          display: flex;
          align-items: center;
          gap: 10px;
          margin-bottom: 10px;
      }

      .captcha-img {
          height: 40px;
          cursor: pointer;
          border: 1px solid #ccc;
      }
  </style>
</head>
<body>


<h2>图形验证码测试</h2>

<div class="captcha-box">
  <img id="captcha-img" class="captcha-img" title="点击刷新验证码" alt="显示失败" />

  <label for="code">
    <input type="text" id="code" placeholder="请输入验证码" />
  </label>

  <button onclick="verifyCaptcha()">提交验证</button>
</div>

<p id="result"></p>

<script>
  let captchaId = '';

  // 加载验证码
  async function loadCaptcha() {
    try {
      const res = await axios.get('http://localhost:3000/captcha', {
        responseType: 'text',
      });


      const svg = res.data;
      console.log(svg);
      captchaId = res.headers['captcha-id'];

      const img = document.getElementById('captcha-img');
      img.src = 'data:image/svg+xml;base64,' + btoa(svg);
    } catch (err) {
      console.error('验证码加载失败:', err);
      alert('验证码加载失败');
    }
  }

  // 提交验证码校验
  async function verifyCaptcha() {
    const code = document.getElementById('code').value;

    try {
      const res = await axios.post('http://localhost:3000/captcha/verify', {
        captchaId,
        code,
      });

      document.getElementById('result').innerText = '✅ 验证成功';
      await loadCaptcha(); // 验证成功后刷新新验证码
    } catch (err) {
      document.getElementById('result').innerText = '❌ 验证失败或过期';
      await loadCaptcha(); // 验证失败后刷新新验证码
    }
  }

  // 点击图片刷新
  document.getElementById('captcha-img').addEventListener('click', loadCaptcha);

  // 初始化
  loadCaptcha();
</script>

</body>
</html>

这里展示的是最简单的,自己可以扩展一下使用短信验证等方式

Copyright © 2019- zgxue.com 版权所有 京ICP备2021021884号-5

违法及侵权请联系:TEL:199 1889 7713 E-MAIL:2724546146@qq.com

本站由北京市万商天勤律师事务所王兴未律师提供法律服务