跳至主要內容

验证码

chanchaw大约 6 分钟languagejava

概述

制作图形验证码,要求用户做简单的四则运算填写计算结果,服务端每次生成一个图片保存在服务器上,返回给前端验证码图片的路径以及正确的计算结果,前端可自行判断用户输入的计算结果是否正确,正确后再执行请求后端的API。

防刷

新用户注册时,会发送验证码给新用户注册时填写的邮箱地址,为了防止恶意刷这个验证码(一直让后台系统发送验证码到用户邮件),合理的使用 redis 实现的方案是在 kvvalue 中拼接发送验证码时的时间戳,关于网上流传的时间戳上限的问题见自己的 文章open in new windowredis 中保存 kv 的逻辑

  • key - 项目名称:微服务名称:业务名称:浏览器指纹 - 可兼容不同项目使用同一个 redis
  • value - 验证码字符串_成功发送验证码的时间戳

每次创建并发送验证码给用户邮箱之前查找 浏览器指纹redis 中有没有对应的数据,拿到数据 value 后,解析出后面的拼接的时间戳,对比当前的时间点,超过 interval 则可以再次发送,相反则提示用户不可频繁操作 - 防刷。

实现

依赖

<dependency>
    <groupId>com.github.penggle</groupId>
    <artifactId>kaptcha</artifactId>
    <version>2.3.2</version>
</dependency>

配置验证码生成规则

需要在 springboot 项目的配置类中制作函数来生成带有验证码生成参数的 bean,配置类的完整代码如下,注意修改下面代码块中的

properties.put("kaptcha.textproducer.impl", "com.cc.visitor.config.KaptchaTextCreator"); 指定的 KaptchaTextCreator ,主要保证包路径是正确的

package com.cc.visitor.config;

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;

/**
 * @author chanchaw
 * @create 2024-10-10 15:10
 */
@Configuration
public class SystemConfig {

    /**
     * 2024年10月10日 15:11:43 依赖 com.github.penggle.kaptcha
     * 的验证码工具配置
     * @return
     */
    @Bean("kaptcha")
    public DefaultKaptcha getDefaultKaptcha(){
        com.google.code.kaptcha.impl.DefaultKaptcha defaultKaptcha = new com.google.code.kaptcha.impl.DefaultKaptcha();
        Properties properties = new Properties();
        properties.put("kaptcha.border", "no");
        properties.put("kaptcha.textproducer.font.color", "red");
        properties.put("kaptcha.image.width", "170");
        properties.put("kaptcha.image.height", "65");
        properties.put("kaptcha.textproducer.font.size", "45");
        properties.put("kaptcha.session.key", "verifyCode");
        properties.put("kaptcha.textproducer.char.space", "6");
        properties.put("kaptcha.obscurificator.impl", "com.google.code.kaptcha.impl.WaterRipple");
        
        // 注意部署到 centos7 中时要保证系统中已经安装中文字体,否则验证码图片中是乱码
        // windows 系统中可以注释下面一行代码,centos7 系统中安装中文字体见文章:
		// https://xdfznh.club/kb/develop/operation-system/linux/centos/系统管理.html#安装中文字体
        // properties.put("kaptcha.textproducer.font.names", "宋体,楷体,微软雅黑");
        properties.put("kaptcha.background.clear.from", "yellow");
        properties.put("kaptcha.background.clear.to", "green");

        // 验证码的长度,原本默认验证码的长度是5,即要求用户输入5个字符(数字+英文字母)
        // 这里可以配置要求用户输入的字符串的长度
        // 后来制作了自定义验证码文本生成器:com.cc.visitor.config.KaptchaTextCreator
        // 所以下面的长度配置是无效的
        properties.put("kaptcha.textproducer.char.length", "4");
        // 自定义验证码文本生成器,本案例中生成一个简单的四则运算的字符串,并且会返回运算的正确结果
        properties.put("kaptcha.textproducer.impl", "com.cc.visitor.config.KaptchaTextCreator");
        Config config = new Config(properties);
        defaultKaptcha.setConfig(config);
        return defaultKaptcha;
    }
}

验证码文本生成器

package com.cc.visitor.config;

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

import java.util.Random;

/**
 * @author chanchaw
 * @create 2024-10-10 13:25
 */
public class KaptchaTextCreator extends DefaultTextCreator {
    private static final String[] NUMBER= "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 chinese = new StringBuilder();
        int randomop = (int) random.nextInt(4);
        //判断结果生成加减乘除
        switch (randomop){
            case 0 :
                result = x * y;
                chinese.append(NUMBER[x]);
                chinese.append("*");
                chinese.append(NUMBER[y]);
                break;
            case 1 :
                if (x == 0 && y % x == 0) {
                    result = y / x;
                    chinese.append(NUMBER[y]);
                    chinese.append("/");
                    chinese.append(NUMBER[x]);
                } else {
                    result = x + y;
                    chinese.append(NUMBER[x]);
                    chinese.append("+");
                    chinese.append(NUMBER[y]);
                }
                break;
            case 2 :
                if (x >= y) {
                    result = x - y;
                    chinese.append(NUMBER[x]);
                    chinese.append("-");
                    chinese.append(NUMBER[y]);
                } else {
                    result = y - x;
                    chinese.append(NUMBER[y]);
                    chinese.append("-");
                    chinese.append(NUMBER[x]);
                }
                break;
            default:
                result = x + y;
                chinese.append(NUMBER[x]);
                chinese.append("+");
                chinese.append(NUMBER[y]);
        }
        //拼接结果返回
        chinese.append("=?" + result);
        //chinese.append("=?");
        return chinese.toString();
    }
}

验证码生成工具

方法 getVerifyCode 是网上拷贝来的生成验证码的方法,前端请求 https://域名/visitorbe/kaptcha/login/getVerifyCode?loginKey=cc ,后端生成验证码图片后直接写入到响应结果中,所以本方法要想要同时向前端响应正确的计算结果还要通过 httpServletResponse.addHeader("kaptchaResult","chanchaw"); 实现,采用本方法时前端用于显示验证码的图片的方法是

<image src="https://域名/visitorbe/kaptcha/login/getVerifyCode?loginKey=cc" mode=""/>

本案例中还制作了方法 createKaptchaImg ,会将生成的验证码生成为一个图片保存到服务器指定的目录下,返回给前端为一个 map 对象,其中包含验证码图片的 url 和正确的计算结果。本方法要求 springboot 项目同时配置 WebMvcConfigure,用于前端采用 url 显示验证码图片

package com.cc.visitor.service;

import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.google.code.kaptcha.servlet.KaptchaServlet;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.imageio.ImageIO;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

/**
 * @author chanchaw
 * @create 2024-10-10 13:00
 */
@Component
public class KaptchaService {
    @Value("${file.kaptcha.img-path}")
    private String imgPath;
    @Value("${file.kaptcha.virtual-path}")
    private String virtualPath;
    @Autowired
    private DefaultKaptcha captchaProducer;
    private Map<String, String> captchaMap = new HashMap<>();
    // 返回前端显示图片用的URL
    public Map<String, String> createKaptchaImg() throws IOException {
        //生产验证码字符串并保存到session中
        String resString = captchaProducer.createText();// 携带了结算结果:1+3=?4
        int i = resString.indexOf("?");
        String verifyCode = resString.substring(0,i+1);
        String result = resString.substring(i+1);
        BufferedImage challenge = captchaProducer.createImage(verifyCode);
        UUID uuid = UUID.randomUUID();
        String fileName = uuid + ".jpg";// 纯文件名
        String fileNameAndPath = imgPath + fileName;// 保存到本地的文件绝对路径+文件名
        File outputfile = new File(fileNameAndPath);
        ImageIO.write(challenge, "jpg", outputfile);

        Map<String, String> ret = new HashMap<>();
        String url = virtualPath.substring(0,virtualPath.length() - 2) + fileName;// url = /kaptcha/sdfsdf.jpg
        ret.put("fileName",url);
        ret.put("result", result);
        return ret;
    }
    public void getVerifyCode(String loginKey, HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws IOException {
        ByteArrayOutputStream imgOutputStream = new ByteArrayOutputStream();
        try {
            //生产验证码字符串并保存到session中
            String verifyCode = captchaProducer.createText();// 携带了结算结果:1+3=?4
            // httpServletRequest.getSession().setAttribute("verifyCode", verifyCode);  // 写入会话
            //redisCache.setVerifyInfo(loginKey, verifyCode);   //写入redis
            captchaMap.put(loginKey, verifyCode);//写入内存
            //log.warn("reset verify code key {}, code {}", loginKey, verifyCode);
            BufferedImage challenge = captchaProducer.createImage(verifyCode);
            ImageIO.write(challenge, "jpg", imgOutputStream);
        } catch (IllegalArgumentException | IOException e) {
            httpServletResponse.sendError(HttpServletResponse.SC_NOT_FOUND);
            return;
        }
        byte[] captchaOutputStream = imgOutputStream.toByteArray();
        httpServletResponse.setHeader("Cache-Control", "no-store");
        httpServletResponse.setHeader("Pragma", "no-cache");
        httpServletResponse.setDateHeader("Expires", 0);
        httpServletResponse.setContentType("image/jpeg");

        httpServletResponse.addHeader("kaptchaResult","chanchaw");
        try (ServletOutputStream responseOutputStream = httpServletResponse.getOutputStream()) {
            responseOutputStream.write(captchaOutputStream);
            responseOutputStream.flush();
        } catch (IOException ex) {
            //log.error("find ex in create a new verify Code", ex);
        }
    }
}

WebMvcConfigure

由于生成的验证码图片在服务器上,前端采用 url 显示验证码图片,所以 springboot 项目还要配置 WebMvcConfigure

package com.cc.visitor.config;

import org.apache.catalina.connector.Connector;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.embedded.tomcat.TomcatConnectorCustomizer;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebMvcConfigure implements WebMvcConfigurer {

    @Value("${file.kaptcha.img-path}")
    private String kaptchaImgPath;

    @Value("${file.kaptcha.virtual-path}")
    private String kaptchaVirtualPath;

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        // centos7 中按照下面一行代码的方式需要在配置文件中设置图片文件的绝对路径:/projs/visitor/kaptcha/
        // 如果下面一行中是:file:/ ,则配置文件中图片的绝对路径是:projs/visitor/kaptcha/
        // 对于 windows 系统匹配下面代码的文件路径是:/d:/temp/kaptcha/
        registry.addResourceHandler(kaptchaVirtualPath).addResourceLocations("file:/" + kaptchaImgPath);
        //registry.addResourceHandler(virtualpathQc).addResourceLocations("file:/" + filepathQc);
    }

    @Bean
    public TomcatServletWebServerFactory webServerFactory() {
        TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();
        factory.addConnectorCustomizers(new TomcatConnectorCustomizer() {
            @Override
            public void customize(Connector connector) {
                connector.setProperty("relaxedPathChars", "\"<>[\\]^`{|}");
                connector.setProperty("relaxedQueryChars", "\"<>[\\]^`{|}");
            }
        });
        return factory;
    }
}

配置文件中的两个路径的配置如下

file:
  kaptcha:
    img-path: '/d:/temp/kaptcha/'
    virtual-path: '/kaptcha/**'

前端在接收到后台服务的响应后从 map 对象解构出图片路径和正确的计算结果。本案例初次使用在 “翔轮访客系统” 中小程序里新用户注册的页面中。

调用方法,在控制器中调用方法 KaptchaService.createKaptchaImg

@RequiredArgsConstructor
@Api(tags = "用户controller")
@RestController
@RequestMapping("/api/user/v1")
public class UserController {
    private final KaptchaService kaptchaUtils;

    @ApiOperation("请求验证码")
    @GetMapping("/kaptcha")
    public ResponseResult getKaptcha() throws IOException {
        Map<String, String> kaptchaImg = kaptchaUtils.createKaptchaImg();
        return ResponseResult.ok(kaptchaImg);
    }
}