(15)SprintBoot 2.X 数学公式验证码

1. 使用数学公式验证码

1.1描述
  • 点击秒杀前,先让用户输入数学公式验证码,验证正确才能获取秒杀地址进行秒杀
1.2 好处
  1. 防止恶意的机器人和爬虫,刷票软件恶意频繁点击按钮来刷请求秒杀地址接口的操作
    • 秒杀接口地址的隐藏可以防止恶意用户通过频繁调用接口来请求的操作,但是无法防止机器人,刷票软件恶意频繁点击按钮来刷请求秒杀地址接口的操作
  2. 分散用户的请求
    • 高并发下场景,在刚刚开始秒杀的那一瞬间,迎来的并发量是最大的,减少同一时间点的并发量,将并发量分流也是一种减少数据库以及系统压力的措施(使得1s中来10万次请求过渡为10s中来10万次请求)
1.3 实现细节
  1. 前端通过把商品id作为参数调用服务端创建验证码接口
  2. 服务端根据前端传过来的商品id和用户id生成验证码,并将商品id+用户id作为key,生成的验证码作为value存入redis,同时将生成的验证码输入图片写入imageIO让前端展示
  3. 将用户输入的验证码与根据商品id+用户id从redis查询到的验证码对比,验证成功就返回秒杀接口地址,进入秒杀;验证失败或从redis查询的验证码为空都返回验证失败,刷新验证码重试

2. 代码实现

2.1 前端代码
2.1.1 前端页面代码
  • 一开始验证码和输入框是不可见的(只有秒杀开始才会可见),图片可以点击刷新图片,所以定义refreshVCode方法来刷新图片
  • 点击秒杀按钮后会进行验证码验证,成功后获取秒杀地址。
<td>
    <div class="row">
        <div class="form-inline">
            <img id="verifyCodeImg" width="80" height="32"  style="display:none" onclick="refreshVerifyCode()"/>
            <input id="verifyCode"  class="form-control" style="display:none"/>
            <button class="btn btn-primary" type="button" id="buyButton"οnclick="getMiaoshaPath()">立即秒杀</button>
        </div>
    </div>
    <input type="hidden" name="goodsId"  id="goodsId" />
</td>
2.1.2 前端逻辑代码
  • 如果秒杀没有开始,countDown()每隔一秒钟执行一次,进行倒计时
  • 如果秒杀开始,则把商品id作为参数,访问服务端获取验证码图片,并设置验证码图片、输入框、秒杀按钮可视化
    function countDown() {
        var remainSeconds = $("#remainSeconds").val();
        var timeout;
        if (remainSeconds > 0) {//秒杀还没开始,倒计时
            $("#buyButton").attr("disabled", true);
            $("#miaoshaTip").html("秒杀倒计时:" + remainSeconds + "秒");
            timeout = setTimeout(function () {
                $("#countDown").text(remainSeconds - 1);
                $("#remainSeconds").val(remainSeconds - 1);
                countDown();
            }, 1000);
        } else if (remainSeconds == 0) {//秒杀进行中
            $("#buyButton").attr("disabled", false);
            if (timeout) {
                clearTimeout(timeout);
            }
            $("#miaoshaTip").html("秒杀进行中");
            $("#verifyCodeImg").attr("src", "/miaosha/verifyCode?goodsId="+$("#goodsId").val());
            $("#verifyCodeImg").show();
            $("#verifyCode").show();
        } else {//秒杀已经结束
            $("#buyButton").attr("disabled", true);
            $("#miaoshaTip").html("秒杀已经结束");
            $("#verifyCodeImg").hide();
            $("#verifyCode").hide();
        }
    }
	 function refreshVerifyCode(){
	     $("#verifyCodeImg").attr("src", "/miaosha/verifyCode?goodsId="+$("#goodsId").val()+"&timestamp="+new Date().getTime());
	 }
2.2 Controller层 返回验证码图片接口
    @RequestMapping(value="/verifyCode", method=RequestMethod.GET)
    @ResponseBody
    public Result<String> getMiaoshaVerifyCode(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);
        }
    }
2.3 MiaoshaService层 生成验证码图片
  • 服务端根据前端传过来的商品id和用户id生成验证码,并将商品id+用户id作为key,生成的验证码作为value存入redis,同时将生成的验证码输入图片写入imageIO让前端展示
  • 图片是利用BufferedImage 类生成,指定高度与宽度,利用Graphics做画笔,填充颜色,画出边界线等操作,然后利用drawString方法将我们随机拼接成字符串写在生成的图片上,还要计算出字符串的值存在缓存里面
  • 利用scriptEngine类,调用JavaScript的eval() 方法,计算这个字符串公式的值,将这个值保存到redis上面去(用户下次发送验证请求的时候,直接去缓存里面取出并验证即可)注意:eval() 计算得到的是double 值,但我们需要的int 值,需要强转
    /**
     * 生成验证码图片
     * @param user
     * @param goodsId
     * @return
     */
    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;
    }

    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;
        }
    }
    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;
    }
2.4 Controller层,当前端点击秒杀按钮后,进入获取秒杀地址业务逻辑,首先进行验证码验证,成功后获取秒杀地址
  • 将用户输入的验证码与根据商品id+用户id从redis查询到的验证码对比,验证成功就返回秒杀接口地址,进入秒杀;验证失败或从redis查询的验证码为空都返回“非法访问”,需要前端重输验证码。
    @AccessLimit(seconds=5, maxCount=5, needLogin=true)
    @RequestMapping(value="/path", method=RequestMethod.GET)
    @ResponseBody
    public Result<String> getMiaoshaPath(HttpServletRequest request, MiaoshaUser user,
                                         @RequestParam("goodsId")long goodsId,
                                         @RequestParam(value="verifyCode", defaultValue="0")int verifyCode
    ) {
        if(user == null) {
            return Result.error(CodeMsg.SESSION_ERROR);
        }
        String path = miaoshaService.createMiaoshaPath(user,goodsId);
        boolean check = miaoshaService.checkVerifyCode(user, goodsId, verifyCode);
        if(!check) {
            return Result.error(CodeMsg.REQUEST_ILLEGAL);
        }
        return Result.success(path);
    }

2.5 MiaoshaService层,进行验证码验证
    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;
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值