在秒杀的场景中还存在着很多的安全问题
- 暴露秒杀地址
- 秒杀请求可以很频繁
- 接口流量大,恶意刷接口
-
隐藏秒杀接口
为什么需要隐藏,事实上,页面上的所有东西都能被客户端拿到,包括js代码,因此,分析商品详情页面就可以知道秒杀的地址所在,如果提前知道秒杀地址,就可以使用提前设置一些代码去刷这个请求接口,造成安全问题。因此需要在点击秒杀按钮的那一刻才知道秒杀地址。这样就没办法提前准备。
因此,在秒杀按钮上,绑定获取秒杀接口的方法,然后通过ajax请求,请求服务器返回一个随机的秒杀地址。
function getMiaoshaPath() { g_showLoading(); //ajax请求 $.ajax({ url:"/miaosha/path", type:"GET", data:{ goodsId:$("#goodsId").val() }, success:function(data){ if(data.code == 0){ var path = data.data; doMiaosha(path); }else{ layer.msg(data.msg); } }, error:function(){ layer.msg("客户端请求有误"); } }); }
返回地址成功,则调用doMiaosha函数,然后请求ajax,url为带有服务器返回的随机秒杀地址的值,这样,秒杀地址就实现了隐藏。
function doMiaosha(path) { $.ajax({ url:"/miaosha/"+path+"/do_miaosha", type:"POST", data:{ goodsId:$("#goodsId").val(), }, success:function(data){ if(data.code == 0){ // window.location.href="/order_detail.htm?orderId="+data.data.id; //code为0,说明秒杀请求已经入队,那么需要客户端发起对服务器的ajax请求,进行轮询。 getMiaoshaResult($("#goodsId").val());//这里将逻辑写成函数 }else{ layer.msg(data.msg); } }, error:function(){ layer.msg("客户端请求有误"); } }); }
-
添加图片验证功能
在页面添加图片验证码之后,需要验证码输入正确,才能执行秒杀,因此,可以有效的防止机器刷接口,而且减低接口的请求并发量。
<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"onclick="getMiaoshaPath()">立即秒杀</button> </div> </div> <input type="hidden" name="goodsId" id="goodsId" />
并通过访问内存,得到早已写入内存的图片缓存流。
比如,当进入商品详情页,会有一个执行秒杀时的倒计时判断,在判断中加入验证码,
function countDown() { //获取剩余时间 var remainSeconds = $("#remainSeconds").val(); //定义超时变量 var timeout; if(remainSeconds>0){ //秒杀还没有开始 //隐藏秒杀的按钮,展示倒计时提醒 $("#buyButton").attr("disabled", true); $("#miaoshaTip").html("秒杀倒计时:"+remainSeconds+"秒"); //利用setTimeout进行时间控制 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(); } }
$("#verifyCodeImg").attr("src", "/miaosha/verifyCode?goodsId="+$("#goodsId").val());
此段代码,就是从后台的路径中取到图片。
@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); } }
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; }
完成一个验证码的功能是比较简单的。
其中Image是一个抽象类,BufferedImage是其实现类,是一个带缓冲区图像类,主要作用是将一幅图片加载到内存中(BufferedImage生成的图片在内存里有一个图像缓冲区,利用这个缓冲区我们可以很方便地操作这个图片),提供获得绘图对象、图像缩放、选择图像平滑度等功能,通常用来做图片大小变换、图片变灰、设置透明不透明等。
通过图片地址的请求可以得到内存中的这个图片,然后显示。
当我们在点击秒杀按钮,获取秒杀的随机路径的时候,就可以根据传过来的验证码信息和已经存在缓存中的验证码信息比较,就可以完成秒杀的验证。
//检查验证码是否正确 boolean check = miaoshaService.checkVerifyCode(user, goodsId, verifyCode); if(!check) { return Result.error(CodeMsg.REQUEST_ILLEGAL); }
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; }
当验证完之后,需要把缓存中的验证码删掉。
-
防盗刷
如果一个用户使用机器不断的请求,则会使并发量增大,因此需要限制一个用户请求的次数,
具体实现比较简单,该用户的每次请求都会统计次数,然后存到缓存中,如果超过一定次数,直接返回错误。
但这种实现没有通用性。
考虑自己创建一个注解,实现统计次数。并返回结果的功能。
-
第一步,新建一个注解
@Retention(RUNTIME) @Target(METHOD) public @interface AccessLimit { //限制秒数 int seconds(); //限制最大次数 int maxCount(); //限制是否要登录 boolean needLogin() default true;//默认是要登录 }
-
第二步,使用拦截器实现注解的功能
@Service public class AccessInterceptor extends HandlerInterceptorAdapter { @Autowired MiaoshaUserService userService; @Autowired RedisService redisService; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //如果这个handler是方法handler,则 if(handler instanceof HandlerMethod){ System.out.println("进来了"); HandlerMethod hm = (HandlerMethod)handler; AccessLimit accessLimit = hm.getMethodAnnotation(AccessLimit.class); //如果没有加注解,不进行拦截。 if(accessLimit==null){ return true; } //取到注解设置的值 int seconds = accessLimit.seconds(); int maxCount = accessLimit.maxCount(); boolean needLogin = accessLimit.needLogin(); //取到用户 MiaoshaUser user = getUser(request, response); //将用户值存到线程中 UserContext.setUser(user); //判断是否需要登录 if(needLogin){ if(user==null){ render(response, CodeMsg.SESSION_ERROR); return false;//表示拦截 } }else { //什么也不错 } //得到请求路径 String key=request.getRequestURI()+"_" + user.getId(); //得到key的前缀以及存活时间 AccessKey ak = AccessKey.withExpire(seconds); Integer count = redisService.get(ak, key, Integer.class); //如果是第一次请求,就存入1 if(count==null){ redisService.set(ak,key,1); }else if(count < maxCount){ //如果数量小于规定的最大请求数,缓存中的值就+1 redisService.incr(ak,key); }else { //返回太频繁的消息 render(response, CodeMsg.ACCESS_LIMIT_REACHED); return false; } } return true;//直接返回不拦截 } //将提示信息转换为json数据返回到页面 private void render(HttpServletResponse response, CodeMsg cm)throws Exception { response.setContentType("application/json;charset=UTF-8"); OutputStream out = response.getOutputStream(); String str = JSON.toJSONString(Result.error(cm)); out.write(str.getBytes("UTF-8")); out.flush(); out.close(); } private MiaoshaUser getUser(HttpServletRequest request, HttpServletResponse response) { String paramToken = request.getParameter(MiaoshaUserService.COOKI_NAME_TOKEN); String cookieToken = getCookieValue(request, MiaoshaUserService.COOKI_NAME_TOKEN); if(StringUtils.isEmpty(cookieToken) && StringUtils.isEmpty(paramToken)) { return null; } String token = StringUtils.isEmpty(paramToken)?cookieToken:paramToken; return userService.getByToken(response, token); } private String getCookieValue(HttpServletRequest request, String cookiName) { Cookie[] cookies = request.getCookies(); if(cookies == null || cookies.length <= 0){ return null; } for(Cookie cookie : cookies) { if(cookie.getName().equals(cookiName)) { return cookie.getValue(); } } return null; } }
-
第三步,将拦截器配置进来
@Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(accessInterceptor); }
将拦截器生效以后,就可以使用注解来设置防盗刷了。