java初探(1)之秒杀的安全

在秒杀的场景中还存在着很多的安全问题

  1. 暴露秒杀地址
  2. 秒杀请求可以很频繁
  3. 接口流量大,恶意刷接口
  • 隐藏秒杀接口

为什么需要隐藏,事实上,页面上的所有东西都能被客户端拿到,包括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);
    }
将拦截器生效以后,就可以使用注解来设置防盗刷了。 

原文地址:https://www.cnblogs.com/lovejune/p/12354975.html

时间: 2024-10-21 01:23:22

java初探(1)之秒杀的安全的相关文章

Java实现高并发秒杀API--Service层2

今天完成了整个Java实现高并发秒杀API--Service层的学习: 1.接口的编码以及实现类的逻辑编写 2.利用spring ioc对Service进行管理 3.利用spring声明式事务对事务进行控制: 事务主要配置: <!--配置事务管理器 -->    <bean id="transactionManager"        class="org.springframework.jdbc.datasource.DataSourceTransacti

Java 实现 淘宝秒杀 聚划算 自动提醒 源码

说明 本实例能够监控聚划算的抢购按钮,在聚划算整点聚的时间到达时自动弹开页面(URL自己定义). 可以自定义监控持续分钟数,同时还可以通过多线程加快刷新速度. 源码 package com.itechzero.pricemonitor; import java.io.BufferedInputStream; import java.io.BufferedReader; import java.io.InputStream; import java.io.InputStreamReader; im

java redis 实现抢购秒杀

2018.10.24 今天研究了下抢购秒杀的功能实现 网上查了一大堆 用redis的最多. 主要是通过redis的 watch multi 事务来控制秒杀数量 不超卖. 这里说下自己的感受: 不超卖的话 那就要一个个的来减库存 这样的话 效率上会有点问题 这里上下代码 基本上是再网上抄的 . 我用的是 springboot jedis 我就直接上代码了 Controller层 package com.bicon.basedemo.controller; import java.util.Arra

java初探(1)之登录总结

登录总结 前几章总结了登录各个步骤中遇到的问题,现在完成的做一个登录的案例,其难点不在于实现功能,而在于抽象各种功能模块,提高复用性,较低耦合度. 前端页面: 对于前端页面来说,不是后端程序员要考虑的事,但为了有备无患,需要了解一些基本的东西,即看的懂即可,原则是,可以不去管css的样式,但js代码还是要多了解. 比如,对于登录页面来说,一般是不会使用表单直接提交,因为有大量的验证工作,因此,需要使用ajax请求技术来完成登录的请求.在请求之前,势必要先对表单上输入的一些内容进行验证,比如,输入

Java 实现 淘宝秒杀 聚划算 自己主动提醒 源代码

说明 本实例可以监控聚划算的抢购button,在聚划算整点聚的时间到达时自己主动弹开页面(URL自定义). 能够自己定义监控持续分钟数,同一时候还能够通过多线程加快刷新速度. 源代码 package com.itechzero.pricemonitor; import java.io.BufferedInputStream; import java.io.BufferedReader; import java.io.InputStream; import java.io.InputStreamR

[shyのJAVA初探]hdu1166●树状数组

一开始shy是为了大数而走近java,随后情不自禁地就希望能初步了解java的语言特点. java初学对c++选手而言可谓简单非常.因为java的语法和c++的语法简直一样(虽然这话不太严谨,容易遭到很多反驳,不过,,shy实在是没有见过如此相像的两种语言).比如,①java开变量的方式是:int x;char c;boolean b;②java的for循环:for(int i=1;i<=n;i++){}③java的条件语句:if(--cas>0&&str!="end

java初探/java读取文件

import java.io.*; import java.util.Arrays; public class WriteText { public static void main(String[] args) throws Exception { FileReader fr=new FileReader("./part-00001"); BufferedReader br=new BufferedReader(fr); String[] info = null; String li

【Java初探外篇02】——关于静态方法与实例方法

在Java的学习中,我们知道,方法的使用是不可或缺的重要部分,在我们编写第一个Java程序hello world的时候,我们就要开始使用主方法main():它就是一个静态方法(static method) . public class sty_01{ //主方法main() public static void main(String[] args){ system.out.println("hello world!"); } } 那么接下来我们具体学习下静态方法与实例方法的区别. 静

[java初探05]__数组的简单认识及Arrays类的常用方法

数组是具有相同数据类型的一组数据的集合.在程序设计中,这样的集合称之为数组.数组的每个元素都具有相同的数据类型,在Java中数组也被看为一个对象. 在里,了解了数组的定义之后, 我们知道了,数组并不是简单的由一组数组成的.而是由一组具有相同数据类型的数据组成的,可以是一组整型的数据,也可以是一组字符型的数组,这里的数可以来理解为代表的是数据,而不是数字的意思. 关于一维数组与二维数组 一维数组实际上就是一组相同数据类型的数据的线性集合. 如果一维数组中的每一个元素任然是一个数组的话,那么它就构成