在前后端分离项目中实现验证码功能

目录

原理

导入验证码依赖

Redis工具类RedisUtils

配置类CaptchaConfig

验证码的文本生成器

在SpringBoot里面配置RedisTemplate

后端返回验证码接口

登录验证(在登录方法之前执行)

Login.vue


原理

通过工具类生成一条算术的验证规则,类似于这样的:1+1=2,其中1+1就是算术规则,2是算术结果。

算术规则我们会通过图片流的形式返回给前端显示出来,让用户看到这个算术规则,计算出结果code。

算术结果我们会存储到redis缓存里面,并设置唯一的一个uuid key,这样我们就可以在用户提交登录表单的时候获取到这个key,也就是uuid。再从redis里面拿到之前缓存的算术结果,再跟用户提交的算术结果code做比较,如果我们生成的算术结果跟用户提交的算术结果code是一致的,那么登录验证通过,否则不通过。

导入验证码依赖

<!--验证码 -->
<dependency>
    <groupId>com.github.penggle</groupId>
    <artifactId>kaptcha</artifactId>
    <version>2.3.2</version>
</dependency>

Redis工具类RedisUtils


import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.connection.RedisConnectionCommands;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

@SuppressWarnings(value = {"unchecked"})
@Component
@Slf4j
public class RedisUtils {
    private static RedisTemplate<String, Object> staticRedisTemplate;

    private final RedisTemplate<String, Object> redisTemplate;

    public RedisUtils(RedisTemplate<String, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    // Springboot启动成功之后会调用这个方法
    @PostConstruct
    public void initRedis() {
        // 初始化设置 静态staticRedisTemplate对象,方便后续操作数据
        staticRedisTemplate = redisTemplate;
    }

    /**
     * 缓存基本的对象,Integer、String、实体类等
     *
     * @param key   缓存的键值
     * @param value 缓存的值
     */
    public static <T> void setCacheObject(final String key, final T value) {
        staticRedisTemplate.opsForValue().set(key, value);
    }

    /**
     * 缓存基本的对象,Integer、String、实体类等
     *
     * @param key      缓存的键值
     * @param value    缓存的值
     * @param timeout  时间
     * @param timeUnit 时间颗粒度
     */
    public static <T> void setCacheObject(final String key, final T value, final long timeout, final TimeUnit timeUnit) {
        staticRedisTemplate.opsForValue().set(key, value, timeout, timeUnit);
    }

    /**
     * 获得缓存的基本对象。
     *
     * @param key 缓存键值
     * @return 缓存键值对应的数据
     */
    public static <T> T getCacheObject(final String key) {
        return (T) staticRedisTemplate.opsForValue().get(key);
    }

    /**
     * 删除单个对象
     *
     * @param key 缓存键值
     */
    public static boolean deleteObject(final String key) {
        return Boolean.TRUE.equals(staticRedisTemplate.delete(key));
    }

    /**
     * 获取单个key的过期时间
     *
     * @param key 缓存键值
     * @return 过期时间
     */
    public static Long getExpireTime(final String key) {
        return staticRedisTemplate.getExpire(key);
    }

    /**
     * 发送ping命令
     * redis 返回pong
     */
    public static void ping() {
        String res = staticRedisTemplate.execute(RedisConnectionCommands::ping);
        log.info("Redis ping ==== {}", res);
    }

    public static Long incr(String key) {
        return staticRedisTemplate.opsForValue().increment(key);
    }

}

配置类CaptchaConfig


import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.google.code.kaptcha.util.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.Properties;

import static com.google.code.kaptcha.Constants.*;

/**
 * 验证码配置
 *
 */
@Configuration
public class CaptchaConfig {

    @Bean
    public DefaultKaptcha getCaptchaBeanMath() {
        DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
        Properties properties = new Properties();
        // 是否有边框 默认为true 我们可以自己设置yes,no
        properties.setProperty(KAPTCHA_BORDER, "yes");
        // 边框颜色 默认为Color.BLACK
        properties.setProperty(KAPTCHA_BORDER_COLOR, "200,200,200");
        // 验证码文本字符颜色 默认为Color.BLACK
        properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_COLOR, "blue");
        // 验证码图片宽度 默认为200
        properties.setProperty(KAPTCHA_IMAGE_WIDTH, "160");
        // 验证码图片高度 默认为50
        properties.setProperty(KAPTCHA_IMAGE_HEIGHT, "40");
        // 验证码文本字符大小 默认为40
        properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_SIZE, "35");
        // KAPTCHA_SESSION_KEY
        properties.setProperty(KAPTCHA_SESSION_CONFIG_KEY, "kaptchaCodeMath");
        // 验证码文本生成器
        properties.setProperty(KAPTCHA_TEXTPRODUCER_IMPL, "com.ttl.common.config.CaptchaTextCreator");
        // 验证码文本字符间距 默认为2
        properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_SPACE, "3");
        // 验证码文本字符长度 默认为5
        properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "6");
        // 验证码文本字体样式 默认为new Font("Arial", 1, fontSize), new Font("Courier", 1, fontSize)
        properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_NAMES, "Arial,Courier");
        // 验证码噪点颜色 默认为Color.BLACK
        properties.setProperty(KAPTCHA_NOISE_COLOR, "white");
        // 干扰实现类
        properties.setProperty(KAPTCHA_NOISE_IMPL, "com.google.code.kaptcha.impl.NoNoise");
        // 图片样式 水纹com.google.code.kaptcha.impl.WaterRipple 鱼眼com.google.code.kaptcha.impl.FishEyeGimpy 阴影com.google.code.kaptcha.impl.ShadowGimpy
        properties.setProperty(KAPTCHA_OBSCURIFICATOR_IMPL, "com.google.code.kaptcha.impl.ShadowGimpy");
        Config config = new Config(properties);
        defaultKaptcha.setConfig(config);
        return defaultKaptcha;
    }

}

验证码的文本生成器

通过这个生成器生成算术的规则


import com.google.code.kaptcha.text.impl.DefaultTextCreator;

import java.util.Random;

/**
 * 验证码文本生成器
 */
public class CaptchaTextCreator extends DefaultTextCreator {
    private static final String[] CNUMBERS = "0,1,2,3,4,5,6,7,8,9,10".split(",");

    @Override
    public String getText() {
        Integer result = 0;
        Random random = new Random();
        int x = random.nextInt(10);
        int y = random.nextInt(10);
        StringBuilder suChinese = new StringBuilder();
        int randomoperands = (int) Math.round(Math.random() * 2);
        if (randomoperands == 0) {
            result = x * y;
            suChinese.append(CNUMBERS[x]);
            suChinese.append("*");
            suChinese.append(CNUMBERS[y]);
        } else if (randomoperands == 1) {
            if (!(x == 0) && y % x == 0) {
                result = y / x;
                suChinese.append(CNUMBERS[y]);
                suChinese.append("/");
                suChinese.append(CNUMBERS[x]);
            } else {
                result = x + y;
                suChinese.append(CNUMBERS[x]);
                suChinese.append("+");
                suChinese.append(CNUMBERS[y]);
            }
        } else if (randomoperands == 2) {
            if (x >= y) {
                result = x - y;
                suChinese.append(CNUMBERS[x]);
                suChinese.append("-");
                suChinese.append(CNUMBERS[y]);
            } else {
                result = y - x;
                suChinese.append(CNUMBERS[y]);
                suChinese.append("-");
                suChinese.append(CNUMBERS[x]);
            }
        } else {
            result = x + y;
            suChinese.append(CNUMBERS[x]);
            suChinese.append("+");
            suChinese.append(CNUMBERS[y]);
        }
        suChinese.append("=?@" + result);
        return suChinese.toString();
    }
}

在SpringBoot里面配置RedisTemplate


import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
@EnableCaching
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        // 设置 key的序列化方式  防止默认的jdk序列化方式出现二进制码  看不懂
        redisTemplate.setKeySerializer(new StringRedisSerializer());

        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance ,
                ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(objectMapper, Object.class);

        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer); // value的序列化类型

        redisTemplate.setConnectionFactory(connectionFactory);
        return redisTemplate;
    }
}

后端返回验证码接口

 /**
     * 获取图形算术验证码
     */
    @GetMapping("/captcha")
    public Result getCaptcha() {
        // 验证码存储到redis
        String uuid = IdUtil.fastSimpleUUID();
        String captchaKey = Constants.REDIS_KEY_CAPTCHA + uuid;

        // 1+1=2   1+1@2
        String captchaText = producer.createText();
        String captchaStr = captchaText.substring(0, captchaText.lastIndexOf("@"));// 1+1
        String captchaCode = captchaText.substring(captchaText.lastIndexOf("@") + 1);// 2
        // 将算术运算结果存储到redis
        RedisUtils.setCacheObject(captchaKey, captchaCode, Constants.CAPTCHA_EXPIRE_MINUTES, TimeUnit.MINUTES);
        // 返回图片的base64编码
        try (FastByteArrayOutputStream outputStream = new FastByteArrayOutputStream()){
            BufferedImage image = producer.createImage(captchaStr);
            ImageIO.write(image, "jpg", outputStream);
            Map<String, Object> map = new HashMap<>();
            map.put("uuid", uuid);
            map.put("img", Base64.encode(outputStream.toByteArray()));
            return Result.success(map);
        } catch (Exception e) {
            log.error("生成验证码错误", e);
            return Result.error("获取验证码错误");
        }
    }

登录验证(在登录方法之前执行)

String uuid = user.getUuid();
String captchaKey = Constants.REDIS_KEY_CAPTCHA + uuid;
String captchaCode = RedisUtils.getCacheObject(captchaKey);
if (captchaCode == null) {
    throw new CustomException("验证码已失效");
}
if (!user.getCode().equals(captchaCode)) {
    throw new CustomException("验证码错误");
}
// 验证完成后删除redis缓存
RedisUtils.deleteObject(captchaKey);

Login.vue

<template>
  <div class="login-container">
    <div class="login-box">
      <div style="font-weight: bold; font-size: 24px; text-align: center; margin-bottom: 30px; color: #EA5455">欢 迎 登 录</div>
        <el-form-item prop="code">
          <div style="display: flex; align-items: center; grid-gap: 5px">
            <el-input size="large" v-model="data.form.code" placeholder="请输入验证码" style="width: 150px"></el-input>
            <div style="flex: 1;">
              <img @click="getCaptchaImg" :src="data.captchaImg" alt="" style="width: 100%; height: 40px; display: block">
            </div>
          </div>
        </el-form-item>
        <el-form-item>
          <el-button size="large" type="primary" style="width: 100%; background-color: #EA5455; border-color: #EA5455" @click="login">登 录</el-button>
        </el-form-item>
      </el-form>
    </div>

  </div>
</template>

<script setup>
import { ref, reactive } from "vue";
import request from '@/utils/request.js'
import {ElMessage} from "element-plus";

const data = reactive({
  form: {},
  rules: {
    code: [
      { required: true, message: '请输入验证码', trigger: 'blur', }
    ],
  },
  captchaImg: ''
})

const formRef = ref()

const getCaptchaImg = () => {
  request.get('/captcha').then(res => {
    if (res.code === '200') {
      data.form.uuid = res.data.uuid
      data.captchaImg = "data:image/gif;base64," + res.data.img
    } else {
      ElMessage.error(res.msg)
    }
  })
}

getCaptchaImg()

</script>

<style scoped>
.login-container {
  height: 100vh;
  overflow:hidden;
  display: flex;
  justify-content: center;
  align-items: center;
  background-image: linear-gradient( 135deg, #FEB692 10%, #EA5455 100%);
}
.login-box {
  width: 350px;
  padding: 30px;
  border-radius: 5px;
  box-shadow: 0 0 10px rgba(0, 0, 0,.1);
  background-color: rgba(255, 255, 255, .5);
}
</style>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值