Java前后端分离验证码处理思路

为什么引入验证码:

        用户注册时:复杂的验证码一定程度上可以防止“恶意注册”,用户在注册时,先校验其验证码

                             是否正确,若验证码错误,则注册失败;

        免密登录时:复杂的验证码一定程度上可以有效防止用户无限点击发送短信验证码接口进行

                             免密登录,造成无用的短信扣费;

流程:

        1:用户第一次进入注册页面时,前端产生一个“唯一标识(此标识作为验证码存放在Redis中的

             Key)”并且前端将该标识存储在sessionStorage中,在注册页面加载时将标识传给后端,

            后端生成验证码将验证码的值作为value存放在Redis中,并将生成的图片验证码Base64数据

           返回给前端渲染;

       2:假如用户刷新注册页面(非第一次进入注册页面),前端则先去sessionStorage中判断是否已

             经存在的“唯一标识”,如果有则将该标识传入后端,后端重新生成验证码并存放到Redis中

            (Redis对已有的Key执行覆盖操作),后端再将新生成的图片验证码Base64数据返回给前端

             渲染;

      3:用户提交注册信息时,不仅要提交用户本身的信息如姓名,性别等信息,还要将用户输入

           的验证码值,和前端生成的验证码标识一起提交给后端,后端根据前端提交过来的验证码标

          识,去Redis中取验证码数据与用户填写的验证码数据做对比,如对比异常则提示“验证码错

          误或失效”,如果验证码校验通过则进一步校验用户信息完成注册;

注意:

        1:为什么要前端产生验证码“唯一标识”作为Redis的Key,而不是后端产生?

                假设验证码的唯一标识在后端产生,用户进入注册界面时,后端产生一个验证码存入Redis并返回给前端,假如用户频繁刷新注册页面则,Redis中会存放很多无用验证码,此方式对于后端来说不是很方便将用户验证码的Key与用户多次操作对应起来(未登录状态);

          唯一标识交由前端产生,可以将同一个用户的多次操作(刷新等操作)与后台Redis库中已有的验证码Key对应起来,Redis会根据Key相同进行覆盖value值,不会产生多个无效验证码;

        2:为什么前端产生的“唯一标识”要存放在sessionStorage中而不是存放localStorage中?

                存放在localStorage中的数据,不手动去清除会一直存在,而存放在sessionStorage中的

                数据会在当前会话结束时自动清除里面的数据;

        3:保存在Redis中的验证码信息,需设置过期时间,比如5分钟,如果不设置过期时间则

             Redis中会一直存放很多已经验证过的验证码,占用Redis空间;

        4:生产环境中可能不止一个业务会用到验证码,比如登录业务也可能会用到验证码,建议前

              端生成的验证码唯一标识带一个验证码类型,用来标识该验证码是什么业务类型的;

                如:type01代表的是注册业务的验证码,type02代表是登录业务的验证码(#作为分隔

                        符,#前数据表示业务类型,#后数据表示验证码的唯一标识);

                        type01#9527

                        type02#9816

      5:生成的图片验证码中有大小写字母,建议后台生成验证码值时全转小写或全转大写再存入

          Redis,在接收到用户输入的验证码进行对比时,同样对用户输入的验证码全转小写或全转

         大写进行比较;

如何生成图片验证码:

        图片验证码的实质:

                一串随机字符串 + 背景图 + 干扰线

图片验证码生成工具类:

package cn.test.util;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.Base64;
import java.util.Random;
 
/**
 * 验证码生成工具类
 */
public class VerifyCodeUtils {
    //使用到Algerian字体,系统里没有的话需要安装字体,字体只显示大写,去掉了1,0,i,o容易混淆的字符
    public static final String VERIFY_CODES = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ";
    private static Random random = new Random();
 
 
    /**
     * 使用系统默认字符源生成验证码
     *
     * @param verifySize 验证码长度
     * @return
     */
    public static String generateVerifyCode(int verifySize) {
        return generateVerifyCode(verifySize, VERIFY_CODES);
    }
 
    /**
     * 使用指定源生成验证码
     *
     * @param verifySize 验证码长度
     * @param sources    验证码字符源
     * @return
     */
    public static String generateVerifyCode(int verifySize, String sources) {
        if (sources == null || sources.length() == 0) {
            sources = VERIFY_CODES;
        }
        int codesLen = sources.length();
        Random rand = new Random(System.currentTimeMillis());
        StringBuilder verifyCode = new StringBuilder(verifySize);
        for (int i = 0; i < verifySize; i++) {
            verifyCode.append(sources.charAt(rand.nextInt(codesLen - 1)));
        }
        return verifyCode.toString();
    }
 
    /**
     * 输出指定验证码图片流
     */
    public static void outputImage(int w, int h, OutputStream os, String code) throws IOException {
        int verifySize = code.length();
        BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
        Random rand = new Random();
        Graphics2D g2 = image.createGraphics();
        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        Color[] colors = new Color[5];
        Color[] colorSpaces = new Color[]{Color.WHITE, Color.CYAN,
                Color.GRAY, Color.LIGHT_GRAY, Color.MAGENTA, Color.ORANGE,
                Color.PINK, Color.YELLOW};
        float[] fractions = new float[colors.length];
        for (int i = 0; i < colors.length; i++) {
            colors[i] = colorSpaces[rand.nextInt(colorSpaces.length)];
            fractions[i] = rand.nextFloat();
        }
        Arrays.sort(fractions);
 
        g2.setColor(Color.GRAY);// 设置边框色
        g2.fillRect(0, 0, w, h);
 
        Color c = getRandColor(200, 250);
        g2.setColor(c);// 设置背景色
        g2.fillRect(0, 2, w, h - 4);
 
        //绘制干扰线
        Random random = new Random();
        g2.setColor(getRandColor(160, 200));// 设置线条的颜色
        for (int i = 0; i < 20; i++) {
            int x = random.nextInt(w - 1);
            int y = random.nextInt(h - 1);
            int xl = random.nextInt(6) + 1;
            int yl = random.nextInt(12) + 1;
            g2.drawLine(x, y, x + xl + 40, y + yl + 20);
        }
 
        // 添加噪点
        float yawpRate = 0.05f;// 噪声率
        int area = (int) (yawpRate * w * h);
        for (int i = 0; i < area; i++) {
            int x = random.nextInt(w);
            int y = random.nextInt(h);
            int rgb = getRandomIntColor();
            image.setRGB(x, y, rgb);
        }
 
        shear(g2, w, h, c);// 使图片扭曲
 
        g2.setColor(getRandColor(100, 160));
        int fontSize = h - 4;
        Font font = new Font("Algerian", Font.ITALIC, fontSize);
        g2.setFont(font);
        char[] chars = code.toCharArray();
        for (int i = 0; i < verifySize; i++) {
            AffineTransform affine = new AffineTransform();
            affine.setToRotation(Math.PI / 4 * rand.nextDouble() * (rand.nextBoolean() ? 1 : -1), (w / verifySize) * i + fontSize / 2, h / 2);
            g2.setTransform(affine);
            g2.drawChars(chars, i, 1, ((w - 10) / verifySize) * i + 5, h / 2 + fontSize / 2 - 10);
        }
 
        g2.dispose();
        ImageIO.write(image, "jpg", os);
    }
 
    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);
        int g = fc + random.nextInt(bc - fc);
        int b = fc + random.nextInt(bc - fc);
        return new Color(r, g, b);
    }
 
    private static int getRandomIntColor() {
        int[] rgb = getRandomRgb();
        int color = 0;
        for (int c : rgb) {
            color = color << 8;
            color = color | c;
        }
        return color;
    }
 
    private static int[] getRandomRgb() {
        int[] rgb = new int[3];
        for (int i = 0; i < 3; i++) {
            rgb[i] = random.nextInt(255);
        }
        return rgb;
    }
 
    private static void shear(Graphics g, int w1, int h1, Color color) {
        shearX(g, w1, h1, color);
        shearY(g, w1, h1, color);
    }
 
    private static void shearX(Graphics g, int w1, int h1, Color color) {
 
        int period = random.nextInt(2);
 
        boolean borderGap = true;
        int frames = 1;
        int phase = random.nextInt(2);
 
        for (int i = 0; i < h1; i++) {
            double d = (double) (period >> 1)
                    * Math.sin((double) i / (double) period
                    + (6.2831853071795862D * (double) phase)
                    / (double) frames);
            g.copyArea(0, i, w1, 1, (int) d, 0);
            if (borderGap) {
                g.setColor(color);
                g.drawLine((int) d, i, 0, i);
                g.drawLine((int) d + w1, i, w1, i);
            }
        }
 
    }
 
    private static void shearY(Graphics g, int w1, int h1, Color color) {
 
        int period = random.nextInt(40) + 10; // 50;
 
        boolean borderGap = true;
        int frames = 20;
        int phase = 7;
        for (int i = 0; i < w1; i++) {
            double d = (double) (period >> 1)
                    * Math.sin((double) i / (double) period
                    + (6.2831853071795862D * (double) phase)
                    / (double) frames);
            g.copyArea(i, 0, 1, h1, 0, (int) d);
            if (borderGap) {
                g.setColor(color);
                g.drawLine(i, (int) d, i, 0);
                g.drawLine(i, (int) d + h1, i, h1);
            }
 
        }
 
    }
 
    /**
     * 获取随机验证码及其加密图片
     */
    public static String VerifyCode(int w, int h, int size) throws IOException {
        Base64.Encoder encoder = Base64.getEncoder();
        String code = generateVerifyCode(size).toLowerCase(); // 指定size长度验证码
        System.out.println("验证码:" + code);
        ByteArrayOutputStream data = new ByteArrayOutputStream();
        outputImage(w, h, data, code);
        return encoder.encodeToString(data.toByteArray());
    }

    public static void main(String[] args) throws  Exception{
        System.out.println("图片验证码转Base64字符串数据:" + VerifyCode(100, 30, 6)); // 指定图片验证码 宽、高、位数

    }
}

执行工具类中main方法如下结果:

验证码为:ltxjtm

验证码转Base64数据: 

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

将Base64数据响应给前端渲染:

测试Html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Base64字符串数据转图片验证码</title>
</head>
<body>
    <img src="data:text/html;base64,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">
</body>
</html>

                                                                                                明月万年无前身,照见古今独醒人

                                                                                                公子王孙何必问,虚度我青春;

  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
前后端分离的架构中,验证码的生成和验证应该由后端实现,前端只负责将用户输入的验证码传输到后端进行验证。具体实现步骤如下: 1. 后端生成验证码 后端可以使用Java的UUID类生成唯一的验证码字符串,然后将其保存到Session中或者放到Redis等缓存中,以便后续验证使用。示例代码如下: ```java String uuid = UUID.randomUUID().toString().replaceAll("-", ""); String verifyCode = VerifyCodeUtil.generateVerifyCode(4); redisTemplate.opsForValue().set(uuid, verifyCode, 5, TimeUnit.MINUTES); // 将验证码保存到缓存中,有效期为5分钟 return uuid; ``` 2. 后端将验证码返回给前端 后端可以将生成的验证码(例如uuid)作为响应返回给前端前端可以将其保存在本地,以便用户在提交表单时一并传输给后端。示例代码如下: ```java @RequestMapping("/code") public String getCode(HttpServletResponse response) throws IOException { String uuid = verifyCodeService.generateVerifyCode(); response.setHeader("Access-Control-Expose-Headers", "uuid"); response.setHeader("uuid", uuid); return null; } ``` 3. 前端展示验证码 前端可以使用图片或者文本的形式展示验证码,用户需要在提交表单时输入正确的验证码。示例代码如下: ```html <img src="/api/code" alt="验证码"> <input type="text" name="verifyCode" placeholder="请输入验证码"> ``` 4. 前端验证码传输到后端进行验证 用户在提交表单时,前端需要将验证码(例如uuid)和用户输入的验证码一起传输到后端进行验证。后端可以从Session或者缓存中获取生成的验证码进行比对,判断用户输入的验证码是否正确。示例代码如下: ```java String uuid = request.getHeader("uuid"); String verifyCode = request.getParameter("verifyCode"); String code = redisTemplate.opsForValue().get(uuid); if (code == null || !verifyCode.equalsIgnoreCase(code)) { return "验证码错误"; } ``` 以上是一个简单的验证码实现过程,具体实现还需要根据自己的业务需求进行调整。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值