SpringBoot+Redis实现图形验证码

Java
378
0
0
2023-04-29

写在前面

在实际生活中,我们经常会遇到在登陆的时候,需要输入图形验证码这样的场景,验证码不仅可以防止爬虫的抓取,还可以限制接口短时间内被访问的次数,可以说也是一种限流措施。本篇来学习如何在前后端分离架构下,基于SpringBoot实现图形验证码这一功能。

实战

项目初始化

第一步,新建一个名为verify-code的SpringBoot项目,并在其POM文件中添加如下依赖:

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
        </dependency>
        
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

创建状态枚举类

第二步,新建一个enums包,并在该包内新建一个名为RespCode的响应状态枚举类:

public enum RespCode {
    SUCCESS(0,"success"),   //成功
    ERROR(1,"error"),   //失败
    ILLEGAL_ARGUMENT(2,"ILLEGAL_ARGUMENT");   //参数错误

    private int code;
    private String desc;

    RespCode(int code, String desc){
        this.code = code;
        this.desc = desc;
    }

    RespCode() {
    }

    public int getCode(){
        return code;
    }

    public String getDesc(){
        return desc;
    }
}

创建响应状态类

第三步,新建entity包,并在该包内新建一个名为RespBean的响应状态类:

public class RespBean<T> implements Serializable {
    private int status;
    private String msg;
    private T data;

    private RespBean(int status){
        this.status=status;
    }

    public RespBean(int status, String msg){
        this.status=status;
        this.msg=msg;
    }

    public RespBean(int status, T data){
        this.status=status;
        this.data=data;
    }

    private RespBean(int status, String msg, T data){
        this.status=status;
        this.msg=msg;
        this.data=data;
    }

    public int getStatus(){
        return status;
    }

    public String getMsg(){
        return msg;
    }

    public T getData(){
        return data;
    }

    public static <T> RespBean<T> ok(){
        return new RespBean<T>(RespCode.SUCCESS.getCode());
    }

    public static <T> RespBean<T> ok(String msg){
        return new RespBean<T>(RespCode.SUCCESS.getCode(),msg);
    }

    public static <T> RespBean<T> ok(T data){
        return new RespBean<T>(RespCode.SUCCESS.getCode(),data);
    }


    public static <T> RespBean<T> ok(String msg,T data){
        return new RespBean<T>(RespCode.SUCCESS.getCode(),msg,data);
    }

    public static <T> RespBean<T> error(){
        return new RespBean<T>(RespCode.ERROR.getCode(),RespCode.ERROR.getDesc());
    }

    public static <T> RespBean<T> error(String errorMessage){
        return new RespBean<T>(RespCode.ERROR.getCode(),errorMessage);
    }

    public static <T> RespBean<T> error(int errorCode,String errorMessage){
        return new RespBean<T>(errorCode,errorMessage);
    }
}

Redis工具类

第四步,新建redis包,并在该包内新建一个名为RedisCache的工具类,该类封装了Redis对字符串类型的操作,即设置值和获取值:

@Component
public class RedisCache {
    @Autowired
    private RedisTemplate redisTemplate;

    public <T> T getCacheObject(final String key){
        ValueOperations<String,T> valueOperations = redisTemplate.opsForValue();
        return valueOperations.get(key);
    }

    public <T> void setCacheObject(final String key, final T value, Integer timeout, TimeUnit timeUnit){
        redisTemplate.opsForValue().set(key,value,timeout,timeUnit);
    }
}

第五步,在redis包内新建一个名为RedisConfig的配置类,该类用于重写Redis的序列化方式。一般来说我们更倾向于在SpringBoot中使用 Spring Data Redis来操作Redis,但是随着而来的则是它的序列化问题,默认使用的是JdkSerializationRedisSerializer,采用的是二进制方式,且会自动的给存入的key和value添加一些前缀,导致实际情况与开发者预想的不一致。针对这种情况我们可以使用Jackson2JsonRedisSerializer这一序列化方式,不建议使用StringRedisTemplate来替代RedisTemplate,因为它提供的数据类型和操作都有限,无法满足日常需要。

定义一个名为RedisConfig的类,该类用于重写RedisTempplate的序列化逻辑,使用Jackson2JsonRedisSerializer取代默认的JdkSerializationRedisSerializer,这样利于后续开发和使用:

@Configuration
public class RedisConfig {
    @Bean
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(connectionFactory);
        // 使用Jackson2JsonRedisSerialize 替换默认序列化(默认采用的是JDK序列化)
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper mapper = new ObjectMapper();
        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(mapper);
        redisTemplate.setKeySerializer(jackson2JsonRedisSerializer);
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.setHashKeySerializer(jackson2JsonRedisSerializer);
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
        return redisTemplate;
    }
}

修改配置文件

第六步,定义一个名为application.yml的配置文件,在里面定义Redis连接信息,同时自定义验证码的一些参数,如缓存中key前缀、验证码过期时间和验证码格式等信息:

spring:
  redis:
    host: 127.0.0.1 # Redis服务器地址
    database: 4 # Redis数据库索引(默认为0)
    port: 6379 # Redis服务器连接端口
    password: root # Redis服务器连接密码(默认为空)
    timeout: 300ms # 连接超时时间(毫秒)
server:
  port: 8888
verify:
  code:
    prefix: verify_code # 缓存中key前缀
    type: jpg # 验证码格式
    timeout: 60 # 验证码过期时间,单位秒

读取配置文件

第七步,新建config包,并在该包内新建一个名为VerifyCodeConfig的属性配置类,该类用于将用户在application.yml配置文件中定义的配置项与VerifyCodeConfig这一属性POJO类进行映射:

@Component
@ConfigurationProperties(prefix = "verify.code")
public class VerifyCodeConfig {
    private String prefix;
    private String type;
    private int timeout;

    //setter和getter方法
}

创建验证码工具类

第八步,新建utils包,并在该包内新建一个名为CodeUtils的工具类,该类用于生成验证码及图片:

public class CodeUtils {
    //验证码字符集
    private static final char[] chars = {'1','2','3','4','5','6','7','8','9',
            'a','b','c','d','e','f','g','h','i','j','k','l','m','n','p','q','r','s','t','u','v','w','x','y','z',
            'A','B','C','D','E','F','G','H','I','J','K','L','M','N','P','Q','R','S','T','U','V','W','X','Y','Z'
    };
    //字符数量
    public static final int SIZE = 4;
    //干扰线数量
    public static final int LINES = 4;
    //图片宽度
    public static final int WIDTH = 200;
    //图片高度
    public static final int HEIGHT = 60;
    //字体大小
    public static final int FONT_SIZE = 30;

    public static Random random = new Random();

    /**
     * 返回验证码字符串和图片BufferedImage对象
     * @return
     */
    public static Object[] createImage(){
        StringBuffer stringBuffer = new StringBuffer();
        //创建空白图片
        BufferedImage image = new BufferedImage(WIDTH,HEIGHT,BufferedImage.TYPE_INT_RGB);
        //获取图片画笔
        Graphics graphics = image.getGraphics();
        //设置画笔颜色
        graphics.setColor(Color.LIGHT_GRAY);
        //绘制矩形背景
        graphics.fillRect(0,0,WIDTH,HEIGHT);
        //画随机字符
        for (int i = 0; i < SIZE; i++) {
            //获取随机字符串索引
            int n = random.nextInt(chars.length);
            //设置随机颜色
            graphics.setColor(getRandomColor());
            //设置字体大小
            graphics.setFont(new Font(null, Font.BOLD + Font.ITALIC,FONT_SIZE));
            //绘制字符
            graphics.drawString(chars[n]+"", i * WIDTH/SIZE, HEIGHT*2/3);
            //记录字符
            stringBuffer.append(chars[n]);
        }
        //画干扰线
        for (int i = 0; i < LINES; i++) {
            //设置随机颜色
            graphics.setColor(getRandomColor());
            //随机画线
            graphics.drawLine(random.nextInt(WIDTH),random.nextInt(HEIGHT),random.nextInt(WIDTH),random.nextInt(HEIGHT));
        }
        //返回验证码和图片
        return new Object[]{stringBuffer.toString(),image};
    }

    /**
     * 随机取色
     * @return
     */
    public static Color getRandomColor(){
        Color color = new Color(random.nextInt(256),random.nextInt(256),random.nextInt(256));
        return color;
    }
}

定义业务处理类

第九步,新建service包,并在该包内新建一个名为VerifyCodeService的业务类,该类用于生成验证码及校验用户输入的验证码是否准确:

@Service
public class VerifyCodeService {
    @Autowired
    private VerifyCodeConfig verifyCodeConfig;
    @Autowired
    private RedisCache redisCache;

    public RespBean generateVerifyCode(){
        Map<String,Object> map = new HashMap<>();
        Object[] objects = CodeUtils.createImage();
        //验证码字符串和图片BufferedImage对象
        //获取验证码字符串,全部转为小写
        String codeStr = objects[0].toString().toLowerCase();
        //获取图片BufferedImage对象
        BufferedImage codeImg = (BufferedImage) objects[1];
        //图片Key对象
        String codeKey = System.currentTimeMillis() + "";
        //构造缓存Key
        String cacheKey = verifyCodeConfig.getPrefix() + codeKey;
        //将数据存入缓存
        redisCache.setCacheObject(cacheKey,codeStr, verifyCodeConfig.getTimeout(), TimeUnit.SECONDS);
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        try{
            ImageIO.write(codeImg, verifyCodeConfig.getType(), baos);
        }catch (IOException e){
            e.printStackTrace();
            return RespBean.error(e.getMessage());
        }
        String codePic = new String(Base64.getEncoder().encode(baos.toByteArray()));
        map.put("codeKey",codeKey);
        map.put("codePic",codePic);
        return RespBean.ok("验证码生成成功",map);
    }

    public RespBean checkVerifyCode(String codeKey,String inputCode){
        //构造缓存Key
        String cacheKey = verifyCodeConfig.getPrefix() + codeKey;
        //获取缓存Value
        String cacheValue = redisCache.getCacheObject(cacheKey);
        if(Objects.nonNull(cacheValue) && cacheValue.equalsIgnoreCase(inputCode)){
            return RespBean.ok("验证码匹配成功");
        }
        return RespBean.error("验证码匹配失败");
    }
}

简单解释一下上述代码的含义:(1)定义generateVerifyCode()方法用于生成图形验证码,然后构建一个返回Map对象,接着构造图片key对象,这个需要在用户请求成功并返回验证码的时候一并携带过去,目的就是后续可以构造缓存key进而从缓存中取出生成的验证码并与用户输入提交的验证码进行对比,进而判断用户验证码是否输入正确;(2)图片key对象这里比较简单,直接采用了时间戳,开发者还可以采用UUID或者其他分布式环境下能唯一标识请求的信息;(3)然后调用mageIO.write()方法通过IO流形式将图片写入到ByteArrayOutputStream 中,并将其转成一个Base64字符串添加到返回Map对象中。当然如果你不是前后端分离的架构,可以将其存入Session中,然后从Session中通过session.getAttribute()方法来获取验证码字符串,而图片直接可通过前端显示在页面上;(4)checkVerifyCode()方法就是从缓存中取出返给前端的图形验证码中的验证码字符串,然后与用户输入提交的字符串进行对比,如果校验通过,则说明验证码匹配成功,反之匹配失败。

定义业务控制器类

第十步,新建controller包,并在该包内新建一个名为VerifyCodeController的控制器类,该类用于提供生成验证码及校验用户输入验证码是否准确的API:

@RestController
public class VerifyCodeController {
    @Autowired
    private VerifyCodeService verifyCodeService;

    @GetMapping("/generateVerifyCode")
    public RespBean generateVerifyCode(){
        return verifyCodeService.generateVerifyCode();
    }

    @PostMapping("/checkVerifyCode")
    public RespBean checkVerifyCode(@RequestBody Map<String,String> map){
        //获取图片key对象
        String codeKey = map.get("codeKey");
        //获取用户输入的验证码
        String inputCode = map.get("inputCode");
        return verifyCodeService.checkVerifyCode(codeKey, inputCode);
    }
}

运行项目进行测试

第十一步,启动项目,开始进行测试。打开Postman,按照图示进行操作:

img

可以看到接口返回了成功信息,但是用户无法直接看到生成的图形验证码,只能看到Base64字符串:

{
    "status": 0,
    "msg": "验证码生成成功",
    "data": {
        "codeKey": "1653484305664",
        "codePic": "/9j/4AAQSkZJRgABAgAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcp"
    }
}

其实我们可以在Postman的Tests模块,从接口中取出返回的Base64字符串,然后构建HTML字符串模板,由于我们返回的Base64字符串中不包含data:image/jpg;base64,这段标志,因此需要在前面补上,最后将得到的信息设置到visualizer中:

//1、将接口返回数据赋值
var data = {
    response:pm.response.json()
}
//2、构建HTML模板字符串
//如果base64代码中没有包含"data:image/jpg;base64,",那么就需要在base64代码前添加
var template = `<html><img src="data:image/jpg;base64,{{response.data.codePic}}"/></html>`;

//3、设置visualizer数据,传入模板并进行解析
pm.visualizer.set(template,data);

注意代码的添加位置,然后再次请求一下生成图形验证码的接口,点击右侧Body区域的Visualizer,可以看到图形验证码已经出现了:

img

接着按照图示操作来校验图形验证码,在Body区域选择raw,然后以JSON形式传入之前返回的codeKey以及用户输入的inputCode:

img

可以看到请求返回成功,并显示验证码匹配成功。

小结

本篇基于SpringBoot+Redis实现了生成和校验图形验证码的功能,原理就是先生成图形验证码及验证码字符串,然后将验证码字符串存入缓存中,接着将图形验证码及字符串key返回给用户,后续用户在提交验证码时,根据字符串key及输入的验证码,从缓存中取出验证码字符串,并与用户输入提交的验证码进行对比,进而判断是否匹配成功。

在了解这种原理之后,你就可以举一反三,利用SpringBoot+Redis这一组合拳实现发送和校验短信验证码,接口防刷、防重复提交等功能。