【商城秒杀项目】-- 使用数学图形验证码来进行限流

秒杀接口地址隐藏可以防止恶意用户通过频繁调用接口来请求的操作,但是无法防止机器人,刷票软件还是可以恶意频繁点击按钮来刷请求秒杀地址接口

高并发下场景,在刚刚开始秒杀的那一瞬间,迎来的并发量是最大的,减少同一时间点的并发量,将并发量分流也是一种减少数据库以及系统压力的措施(使得1s中来10万次请求过渡为10s中来10万次请求)

本篇博客记录如何使用数学图形验证码来进行限流,限流削峰其他操作可参考:

【商城秒杀项目】-- 流量削峰应该怎么做

实现思路:

点击秒杀之前,需先输入验证码,来分散用户的请求。具体实现是后端生成类似1+2-3的验证码,把结果计算出来存至redis,再把验证码图片发至客户端,此后客户端在请求秒杀地址前输入验证码值发请求验证,后端去缓存里面取值验证是否与用户输入的值相同,验证通过才会动态生成秒杀地址给前端

步骤:

  • 在商品详情页面加入验证码图片标签,指定id,再加入验证码输入框input组件,并初始化它们的属性为不可见的,因为一开始验证码和输入框是不可见的(只有秒杀开始才会可见),可以点击刷新图片,所以定义refreshVCode函数来刷新图片

refreshVCode函数代码如下:

function refreshVerifyCode() {
    $("#verifyCodeImg").attr("src", "/miaosha/verifyCode?goodsId=" + $("#goodsId").val() + "&timestamp=" + new Date().getTime());
}

注:在图片上定义oncilck操作,点击后会请求获取图片验证码的接口,但是浏览器会有缓存,所以加上timestamp这个参数,浏览器才会真正发送请求,不然会去缓存里面拿 

  • 在倒计时方法里面正在进行秒杀分支判断中加入显示验证码以及验证码输入框的代码逻辑,开始秒杀的时候,设置其可见并且指定attr()方法动态指定src,发送请求到后端,动态生成图片;注意秒杀结束之后,又需要将其设置为不可见的

  • 请求中传参为goodsId,然后根据用户id和goodsId生成数学公式验证码,然后将这个验证码图片用response的输出流输出至前端

后端生成验证码图片接口代码:

@RequestMapping(value = "/verifyCode", method = RequestMethod.GET)
@ResponseBody
public Result<String> getMiaoshaVerifyCod(HttpServletResponse response,MiaoshaUser user,
                                          @RequestParam("goodsId") long goodsId) {
    if (user == null) {
        return Result.error(CodeMsg.SESSION_ERROR);
    }
    try {
        BufferedImage image = miaoshaService.createVerifyCode(user, goodsId);
        OutputStream out = response.getOutputStream();
        ImageIO.write(image, "JPEG", out);
        out.flush();
        out.close();
        return null;
    } catch (Exception e) {
        e.printStackTrace();
        return Result.error(CodeMsg.MIAOSHA_FAIL);
    }
}

图片是利用BufferedImage类生成的,指定高度与宽度,利用Graphics做画笔,填充颜色,画出边界线等操作,然后利用drawString方法将验证码随机拼接成字符串写在生成的图片上,还要把计算出字符串的值存在redis里面

createMiaoshaVertifyCode方法代码:

/**
 * 生成验证码
 */
public BufferedImage createVerifyCode(MiaoshaUser user, long goodsId) {
    if (user == null || goodsId <= 0) {
        return null;
    }
    int width = 80;
    int height = 32;
    //create the image
    BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
    Graphics g = image.getGraphics();
    //set the background color
    g.setColor(new Color(0xDCDCDC));
    g.fillRect(0, 0, width, height);
    //draw the border
    g.setColor(Color.black);
    g.drawRect(0, 0, width - 1, height - 1);
    //create a random instance to generate the codes
    Random rdm = new Random();
    //make some confusion
    for (int i = 0; i < 50; i++) {
        int x = rdm.nextInt(width);
        int y = rdm.nextInt(height);
        g.drawOval(x, y, 0, 0);
    }
    //generate a random code
    String verifyCode = generateVerifyCode(rdm);
    g.setColor(new Color(0, 100, 0));
    g.setFont(new Font("Candara", Font.BOLD, 24));
    g.drawString(verifyCode, 8, 24);
    g.dispose();
    //把验证码存到redis中
    int rnd = calc(verifyCode);
    redisService.set(MiaoshaKey.getMiaoshaVerifyCode, user.getId() + "," + goodsId, rnd);
    //输出图片
    return image;
}

对于数学公式的生成,是使用generateVerifyCode方法实现的,生成3个0到9之间的随机数,然后在生成一个字符数组,用于存放 + - * (加减乘)三个数学运算符,随机选中两个字符,然后将其进行拼接成一个字符串,数+运算符+数+运算符+数,返回这个字符串,generateVerifyCode方法代码如下:

private static char[] ops = new char[]{'+', '-', '*'};

/**
 * 生成加减乘的算式
 * + - *
 */
private String generateVerifyCode(Random rdm) {
    int num1 = rdm.nextInt(10);
    int num2 = rdm.nextInt(10);
    int num3 = rdm.nextInt(10);
    char op1 = ops[rdm.nextInt(3)];
    char op2 = ops[rdm.nextInt(3)];
    String exp = "" + num1 + op1 + num2 + op2 + num3;
    return exp;
}
  • 利用scriptEngine类,调用JavaScript的eval() 方法,计算这个字符串公式的值,并将这个值保存到redis里面去(用户下次发送请求的时候,直接去缓存里面取出并验证即可);注意eval()方法计算得到的是double类型的值,但我们需要的是int类型的值,所以需要强转

calc方法代码如下:

/**
 * 将算式进行计算
 */
private static int calc(String exp) {
    try {
        ScriptEngineManager manager = new ScriptEngineManager();
        ScriptEngine engine = manager.getEngineByName("JavaScript");
        return (Integer) engine.eval(exp);
    } catch (Exception e) {
        e.printStackTrace();
        return 0;
    }
}
  • 前端得到这个验证码图片显示该验证码,然后用户需要输入验证码将这个验证码作为参数,与获取秒杀地址请求一起传输给后端(校验的操作在获取秒杀地址之前),后端接收到参数后进行验证码比对,从缓存中取出该验证码进行校验;如果不通过,不生成秒杀接口地址,直接返回验证码错误信息

后端验证逻辑:

checkVerifyCode方法代码:

/**
 * 校验数字公式的验证码结果
 * @param user
 * @param goodsId
 * @param verifyCode
 * @return
 */
public boolean checkVerifyCode(MiaoshaUser user, long goodsId, int verifyCode) {
    if (user == null || goodsId <= 0) {
        return false;
    }
    Integer codeOld = redisService.get(MiaoshaKey.getMiaoshaVerifyCode, user.getId() + "," + goodsId, Integer.class);
    if (codeOld == null || codeOld - verifyCode != 0) {
        return false;
    }
    //删除缓存里的数据
    redisService.delete(MiaoshaKey.getMiaoshaVerifyCode, user.getId() + "," + goodsId);
    return true;
}
发布了133 篇原创文章 · 获赞 94 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/weixin_42687829/article/details/104515151