(15)SprintBoot 2.X 数学公式验证码
1. 使用数学公式验证码
1.1描述
- 点击秒杀前,先让用户输入数学公式验证码,验证正确才能获取秒杀地址进行秒杀
1.2 好处
- 防止恶意的机器人和爬虫,刷票软件恶意频繁点击按钮来刷请求秒杀地址接口的操作
- 秒杀接口地址的隐藏可以防止恶意用户通过频繁调用接口来请求的操作,但是无法防止机器人,刷票软件恶意频繁点击按钮来刷请求秒杀地址接口的操作
- 分散用户的请求
- 高并发下场景,在刚刚开始秒杀的那一瞬间,迎来的并发量是最大的,减少同一时间点的并发量,将并发量分流也是一种减少数据库以及系统压力的措施(使得1s中来10万次请求过渡为10s中来10万次请求)
1.3 实现细节
- 前端通过把商品id作为参数调用服务端创建验证码接口
- 服务端根据前端传过来的商品id和用户id生成验证码,并将商品id+用户id作为key,生成的验证码作为value存入redis,同时将生成的验证码输入图片写入imageIO让前端展示
- 将用户输入的验证码与根据商品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()+"×tamp="+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 值,需要强转
public BufferedImage createVerifyCode(MiaoshaUser user, long goodsId) {
if(user == null || goodsId <=0) {
return null;
}
int width = 80;
int height = 32;
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics g = image.getGraphics();
g.setColor(new Color(0xDCDCDC));
g.fillRect(0, 0, width, height);
g.setColor(Color.black);
g.drawRect(0, 0, width - 1, height - 1);
Random rdm = new Random();
for (int i = 0; i < 50; i++) {
int x = rdm.nextInt(width);
int y = rdm.nextInt(height);
g.drawOval(x, y, 0, 0);
}
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();
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;
}