SpringBoot实现图形验证码生成与校验

介绍

在实现登录或者注册功能里面图形验证码的时候,我们需要生成一个固定长度的随机字符串和该字符串对应的一张图片展示在界面。

用户根据看到的图片输入验证码之后我们可以判断输入验证码是否正确 / 过期。

我们这里使用Redis存储生成的验证码(用户名为key,图形验证码为value)。

功能实现

导入依赖

<!--redis场景启动器-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- redis 连接池 -->
<!--新版本连接池lettuce-->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>
<!--fastjson-->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.83</version>
</dependency>
<!--带有过期时间的Map-->
<dependency>
    <groupId>net.jodah</groupId>
    <artifactId>expiringmap</artifactId>
    <version>0.5.10</version>
</dependency>

图形验证码工具类

import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.util.Random;

public class RandomValidateCodeUtil {
    
    // 随机产生数字与字母组合的字符串
    private static final String randString = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
    private static final int width = 95;// 图片宽
    private static final int height = 25;// 图片高
    private static final int lineSize = 40;// 干扰线数量
    private static final int stringNum = 4;// 随机产生字符数量
    private static final Random random = new Random();// 随机数对象

    /**
     * 获得字体
     */
    private static Font getFont() {
        return new Font("Fixedsys", Font.CENTER_BASELINE, 18);
    }

    /**
     * 获得颜色
     */
    private static Color getRandColor(int fc, int bc) {
        if (fc > 255)
            fc = 255;
        if (bc > 255)
            bc = 255;
        int r = fc + random.nextInt(bc - fc - 16);
        int g = fc + random.nextInt(bc - fc - 14);
        int b = fc + random.nextInt(bc - fc - 18);
        return new Color(r, g, b);
    }

    /**
     * 生成随机图片
     */
    public static String getRandomCode(HttpServletRequest request, HttpServletResponse response) {
        HttpSession session = request.getSession();
        // BufferedImage类是具有缓冲区的Image类,Image类是用于描述图像信息的类
        BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_BGR);
        // 产生Image对象的Graphics对象,改对象可以在图像上进行各种绘制操作
        Graphics g = image.getGraphics();
        g.fillRect(0, 0, width, height);//图片大小
        g.setFont(new Font("Times New Roman", Font.ROMAN_BASELINE, 18));//字体大小
        g.setColor(getRandColor(110, 133));//字体颜色
        // 绘制干扰线
        for (int i = 0; i <= lineSize; i++) {
            drawLine(g);
        }
        // 绘制随机字符
        String randomString = "";
        for (int i = 1; i <= stringNum; i++) {
            randomString = drawString(g, randomString, i);
        }

        g.dispose();

        try {
            // 将内存中的图片通过流动形式输出到客户端
            ImageIO.write(image, "JPEG", response.getOutputStream());
        } catch (Exception e) {
            System.out.println("将内存中的图片通过流动形式输出到客户端失败");
            e.printStackTrace();
        }
        return randomString;
    }

    /**
     * 绘制字符串
     */
    private static String drawString(Graphics g, String randomString, int i) {
        g.setFont(getFont());
        g.setColor(new Color(random.nextInt(101), random.nextInt(111), random
                .nextInt(121)));
        String rand = String.valueOf(getRandomString(random.nextInt(randString
                .length())));
        randomString += rand;
        g.translate(random.nextInt(3), random.nextInt(3));
        g.drawString(rand, 13 * i, 16);
        return randomString;
    }

    /**
     * 绘制干扰线
     */
    private static void drawLine(Graphics g) {
        int x = random.nextInt(width);
        int y = random.nextInt(height);
        int xl = random.nextInt(13);
        int yl = random.nextInt(15);
        g.drawLine(x, y, x + xl, y + yl);

    }

    /**
     * 获取随机的字符
     */
    public static String getRandomString(int num) {
        return String.valueOf(randString.charAt(num));
    }
}

controller层

redis使用字符串类型存储,key使用用户的username,value是生成的随机字符串

import com.lixianhe.utils.RandomValidateCodeUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.configurationprocessor.json.JSONException;
import org.springframework.boot.configurationprocessor.json.JSONObject;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@RestController
@Slf4j
@RequestMapping("/login")
public class PicVerifyAction {

    private final static Logger logger = LoggerFactory.getLogger(PicVerifyAction.class);

    @Resource(name = "redisTemplateDefault")
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 生成验证码
     */
    @RequestMapping(value = "/getVerify/{username}")
    public void getVerify(@PathVariable String username, HttpServletRequest request, HttpServletResponse response) {

        try {
            //设置相应类型,告诉浏览器输出的内容为图片
            response.setContentType("image/jpeg");

            //设置响应头信息,告诉浏览器不要缓存此内容
            response.setHeader("Pragma", "No-cache");
            response.setHeader("Cache-Control", "no-cache");
            response.setDateHeader("Expire", 0);

            // 输出图片,返回验证码
            String randomCode = RandomValidateCodeUtil.getRandomCode(request, response);

            ValueOperations<String, String> opsForValue = stringRedisTemplate.opsForValue();
            // 验证码存储进Redis并设置有效期
            opsForValue.set(username, randomCode, 2, TimeUnit.MINUTES);

        } catch (Exception e) {
            log.error("获取验证码失败>>>>   ", e);
        }
    }

    /**
     * 校验验证码
     */
    @RequestMapping(value = "/checkVerify", method = RequestMethod.POST, headers = "Accept=application/json")
    public int checkVerify(@RequestBody JSONObject jsonObject) throws JSONException {
        // 响应的状态
        int status = 0;
        // 用户名
        String username = (String) jsonObject.get("username");
        // 用户输入的验证码

        String code = (String) jsonObject.get("code");
        try {
            // 从redis数据库中获取并比较
            ValueOperations<String, String> valueOperations = stringRedisTemplate.opsForValue();
            String res = (String) valueOperations.get(username);
            if (res != null && res.equals(code)) {
                status = 1;
            }
        } catch (Exception e) {
            log.error("验证码校验失败", e);
            return status;
        }
         /*
              1 成功
              0 失败
          */
        return status;
    }
}

限制IP请求次数

创建annotation包,定义注解

import java.lang.annotation.*;

@Documented
@Target(ElementType.METHOD) // 说明该注解只能放在方法上面
@Retention(RetentionPolicy.RUNTIME)
public @interface LimitRequest {
    long time() default 6000; // 限制时间 单位:毫秒
    int count() default 3; // 允许请求的次数

}

创建aspect包,定义切面类

import com.lixianhe.annotation.LimitRequest;
import net.jodah.expiringmap.ExpirationPolicy;
import net.jodah.expiringmap.ExpiringMap;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;


@Aspect
@Component
public class LimitRequestAspect {

    private static final ConcurrentHashMap<String, ExpiringMap<String, Integer>> book = new ConcurrentHashMap<>();

    // 定义切点
    // 让所有有@LimitRequest注解的方法都执行切面方法
    @Pointcut("@annotation(limitRequest)")
    public void excudeService(LimitRequest limitRequest) {
    }

    @Around(value = "excudeService(limitRequest)", argNames = "pjp,limitRequest")
    public Object doAround(ProceedingJoinPoint pjp, LimitRequest limitRequest) throws Throwable {

        // 获得request对象
        RequestAttributes ra = RequestContextHolder.getRequestAttributes();
        ServletRequestAttributes sra = (ServletRequestAttributes) ra;
        assert sra != null;
        HttpServletRequest request = sra.getRequest();

        // 获取Map对象, 如果没有则返回默认值
        // 第一个参数是key, 第二个参数是默认值
        ExpiringMap<String, Integer> uc = book.getOrDefault(request.getRequestURI(), ExpiringMap.builder().variableExpiration().build());
        Integer uCount = uc.getOrDefault(request.getRemoteAddr(), 0);
        
        if (uCount >= limitRequest.count()) { // 超过次数,不执行目标方法
            System.out.println("接口请求超过次数!");
            throw new RuntimeException("接口请求超过次数!");
        } else if (uCount == 0) { // 第一次请求时,设置有效时间
            uc.put(request.getRemoteAddr(), uCount + 1, ExpirationPolicy.CREATED, limitRequest.time(), TimeUnit.MILLISECONDS);
        } else { // 未超过次数, 记录加一
            uc.put(request.getRemoteAddr(), uCount + 1);
        }
        book.put(request.getRequestURI(), uc);

        // result的值就是被拦截方法的返回值
        return pjp.proceed();
    }
}

在接口上使用注解

//这个注解就是表示, 你在限制时间里8s, 只能请求五次
@LimitRequest(count = 5,time = 8000)
@RequestMapping(value = "/getVerify/{username}")
public void getVerify(@PathVariable String username, HttpServletRequest request, HttpServletResponse response) {
    ...
}

测试

在浏览器输入ip:port/login/getVerify/{username},会显示验证码图片

在redis数据库会存入key为username,value为验证码的键值对

校验:发送Post请求把验证码和username一起发送,判断Redis中该username对应的验证码与用户输入的验证吗是否相同,如果不同说明,验证码错误,如果找不到,说明验证码已经过期。

多次刷新浏览器获取图型验证码,就会抛出异常。

  • 5
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

龙域、白泽

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值