秒杀项目 (7)安全优化

本文详细介绍了秒杀系统中安全优化的关键策略,包括接口地址隐藏、数学验证码的使用及生成,以及防刷限流措施。通过这些技术手段,有效提升了系统的安全性,防止恶意刷单,确保公平交易。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、安全优化

1.1 思路

  • 秒杀接口地址隐藏
  • 数学公式验证码
  • 接口限流防刷

1.2 隐藏秒杀地址原因

秒杀未开始前,如果抓包先获取path,在将path拼到秒杀地址上,也可以秒杀到商品(ps:不加验证码的情况下)

二、接口地址隐藏

2.1 秒杀之前,先去接口请求获取秒杀地址

  • 思路
    将生成的path存到redis中
@RequestMapping(value = "/path",method = RequestMethod.GET)
    @ResponseBody
    public Result<String> getMiaoshaPath(Model model,MiaoshaUser miaoshaUser,@RequestParam("godsId")Long goodsId){
        model.addAttribute("user",miaoshaUser);
        String path = miaoshaService.createPath(miaoshaUser, goodsId);
        return Result.success(path);
    }

2.2 秒杀时,先验证path

秒杀收到请求,先验证PathVarible ,如果不一一致,则显示错误信息

 String realpath = miaoshaService.getPath(miaoshaUser, goodsId);
        if(!realpath.equals(path)){
            return Result.error(CodeMsg.GETPASS_ERROR);
        }

2.3 接口地址隐藏的具体步骤

  • 验证码验证正确后,进入获取秒杀地址的阶段
  • 随机生成一个字符串,返回给前端页面,并且将字符串作为value,userid+goodsid+“killpath”,作为key存到redis缓存中
  • 用户根据key从redis中取到有关秒杀路径的字符串,与从前端取到的字秒杀路径的字符串进行比对,正确,则将字符串与秒杀路径组合,这样用户就得到真正的秒杀路径。
  • 这样的作用是每个用户,每个商品,每次请求都对应一个真正的秒杀路径,防止其他用户窃取了当前用户的秒杀路径和通过抓包的方式,获取到真正的秒杀路径。

三、数学验证码

3.1 作用

  • 防机器人
  • 分散用户的请求

3.2 生成验证码接口

  @RequestMapping("/verifyCode")
    @ResponseBody
    public Result<Long> getMiaoshaVerifyCode(Model model, MiaoshaUser miaoshaUser, @RequestParam("godsId")Long goodsId, HttpServletResponse response){
        try {
            if(miaoshaUser == null){
                return Result.error(CodeMsg.SESSION_ERROR);
            }
            BufferedImage image = miaoshaService.createVerifyCode(miaoshaUser,goodsId);
            ServletOutputStream outputStream = response.getOutputStream();
            ImageIO.write(image,"JPEG",outputStream);
            outputStream.flush();
            outputStream.close();
            //数据已经通过ServletOutputStream返回,不需要再返回
            return null;
        } catch (IOException e) {
            e.printStackTrace();
            return Result.error(CodeMsg.MIAOSHA_FAIL);
        }
    }

3.2.1 生成带数学公式的验证码,保存到redis中

返回BufferedImage 类型的验证码

  public BufferedImage createVerifyCode(MiaoshaUser miaoshaUser, Long goodsId)
    {
        if(miaoshaUser==null||goodsId<=0) {
            return null;
        }
        int width=80;
        int height=30;
        BufferedImage img=new BufferedImage(width,height,BufferedImage.TYPE_INT_RGB);
        Graphics g=img.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 vertifyCode=generateVertifyCode(rdm);
        //颜色,字体
        g.setColor(new Color(0,100,0));
        g.setFont(new Font("Candara",Font.BOLD,24));
        //将验证码写在图片上
        g.drawString(vertifyCode, 8, 24);
        g.dispose();
        //计算存值
        int rnd=calc(vertifyCode);
        //将计算结果保存到redis上面去
        redisService.set(MiaoshaUserKey.getMiaoshaVertifyCode, ""+miaoshaUser.getId()+"_"+goodsId, rnd);
        return img;
    }

3.2.2 生成验证码上的数学公式

只做加减乘,除法为0会出错

private char[] ops = new char[]{'+','-','*'};
private String generateVertifyCode(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;
    }

3.2.3 返回验证码的结果,便于与输入的进行比较

作用可以解析验证码上的字符串,并进行计算,返回结果

 private static int calc(String exp)
    {
        try {
            ScriptEngineManager scriptEngineManager = new ScriptEngineManager();
            ScriptEngine javaScript = scriptEngineManager.getEngineByName("JavaScript");
            return (Integer)javaScript.eval(exp);
        } catch (ScriptException e) {
            e.printStackTrace();
            return 0;
        }

3.3 点击验证码,就能更换新的验证码

加一个onClick事件

3.4 验证验证码

  • 在获得秒杀路径后,就可以进行验证验证码
  • 验证成功后要删除验证的验证码
  @RequestMapping(value = "/path",method = RequestMethod.GET)
    @ResponseBody
    public Result<String> getMiaoshaPath(Model model,MiaoshaUser miaoshaUser,@RequestParam("godsId")Long goodsId,@RequestParam("verifyCode")int verifyCode){
        if(miaoshaUser == null || goodsId < 0){
            return Result.error(CodeMsg.SESSION_ERROR);
        }
        //验证验证码
        Boolean check = miaoshaService.checkVerifyCode(miaoshaUser,goodsId,verifyCode);
        if(!check){
            return Result.error(CodeMsg.REQUEST_ILLEAGAL);
        }
        model.addAttribute("user",miaoshaUser);
        String path = miaoshaService.createPath(miaoshaUser, goodsId);
        return Result.success(path);
    }

验证验证码具体函数:

 public Boolean checkVerifyCode(MiaoshaUser miaoshaUser, Long goodsId , int verifyCode) {
        Integer integer = redisService.get(MiaoshaUserKey.getMiaoshaVertifyCode, "" + miaoshaUser.getId() + "_" + goodsId, Integer.class);
        if (integer == null || integer - verifyCode != 0) {
            return false;
        } else {//验证完,要删除验证码
            redisService.delete(MiaoshaUserKey.getMiaoshaVertifyCode, "" + miaoshaUser.getId() + "_" + goodsId);
            return true;
        }
    }

3.5 验证码的验证流程

  • 将生成的验证码的结果放在redis缓存中中,userid+goodsid作为key,值作为value存到redis中
  • 用户在前端输入的验证码结果,与从redis中取出的结果进行比对,错误返回验证码输入错误,不用删除redis中的验证码
  • 用户点击验证码,生成新的验证码,会把新的验证码的结果存到redis中,并且将原来的相同的key的值进行覆盖。
  • 比对正确则删除当前redis缓存中的验证码的结果。

四、防刷限流

4. 1 作用

限制一个用户在一段时间内访问的次数

4.2 思路

功能在redis中实现,访问一次对应的值加1,超过访问次数,限制访问,过了一分钟,则重新设置有效期和访问次数。

4.3 简单的防刷限流

  • 思路:用户的 ID和请求的路径作为key,首先从redis中根据该key去取得用户的访问次数,如果为null,则根据该key重新设置key,value,value的值是1,能取到key对应的value,则判断vlaue是否小于5,是则处理请求,否则返回error。
  //2.查询访问的次数
        String requestURI = request.getRequestURI();
        String key = requestURI + miaoshaUser.getId() + "";
        Integer count = redisService.get(AccessKey.accessKey, key, Integer.class);
        if(count == null){
            redisService.set(AccessKey.accessKey, key,1);
        }else if(count < 5) {
            redisService.incr(AccessKey.accessKey, key);
        }else {
            return Result.error(CodeMsg.ACCESS_LIMIT);
        }

4.4 规范的防刷限流

4.4.1 自定义注解

 @AccessLimit(seconds=5,maxCount=10,needLogin=true)

4.4.2 自定义注解

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AccessLimit {
    int seconds();
    int maxCount();
    boolean needLogin() default true;
}

4.4.3 自定义ThreadLocal存放变量

public class UserContext {
    //每个线程中有一个ThreadLocal
    public static ThreadLocal<MiaoshaUser> user = new ThreadLocal<>();
    public static void setUser(MiaoshaUser miaoshaUser){
        user.set(miaoshaUser);
    }
    public static MiaoshaUser getUser(){
        return (MiaoshaUser)user.get();
    }
}

4.5 自定义拦截器斤进行防刷限流

public class AccessInteceptor extends HandlerInterceptorAdapter {
    @Autowired
    MiaoShaUserService miaoShaUserService;
    @Autowired
    RedisService redisService;
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if(handler instanceof HandlerMethod){
            //渠取到用户
            MiaoshaUser user = getUser(request, response);
            HandlerMethod handler1 = (HandlerMethod) handler;
            //拿到注解
            AccessLimit accessLimit = handler1.getMethodAnnotation(AccessLimit.class);
            if(accessLimit == null){
                return true;
            }
            int seconds = accessLimit.seconds();
            int maxCount = accessLimit.maxCount();
            String key = request.getRequestURI();
            boolean needLogin = accessLimit.needLogin();
            if(needLogin){
                if(user == null){
                    render(response, CodeMsg.SESSION_ERROR);
                    return false;
                }
                key += "_" + user.getId();
            }else {
            }
            Integer count = redisService.get(AccessKey.withExpireTime(seconds), key, Integer.class);
            if(count == null){
                redisService.set(AccessKey.withExpireTime(seconds), key,1);
            }else if(count < maxCount) {
                redisService.incr(AccessKey.withExpireTime(seconds), key);
            }else {
                render(response,CodeMsg.ACCESS_LIMIT);
                return false;
            }
        }
        return true;
    }

    private void render(HttpServletResponse response, CodeMsg msg) throws IOException {
        OutputStream outputStream = response.getOutputStream();
        String string = JSON.toJSONString(msg);
        outputStream.write(string.getBytes("UTF-8"));
        outputStream.flush();
        outputStream.close();
    }

    public MiaoshaUser getUser(HttpServletRequest request, HttpServletResponse response){
        String paramToken=request.getParameter(MiaoShaUserService.COOKIE_NAME);
        System.out.println("@UserArgumentResolver-resolveArgument  paramToken:"+paramToken);
        //获取cookie
        String cookieToken=getCookieValue(request,MiaoShaUserService.COOKIE_NAME);
        System.out.println("@UserArgumentResolver-resolveArgument  cookieToken:"+cookieToken);
        if(StringUtils.isEmpty(cookieToken)&& StringUtils.isEmpty(paramToken))
        {
            return null;
        }
        String token=StringUtils.isEmpty(paramToken)?cookieToken:paramToken;
        //System.out.println("goods-token:"+token);
        //System.out.println("goods-cookieToken:"+cookieToken);
        MiaoshaUser user=miaoShaUserService.getByToken(token,response);
        return user;
    }
    public String getCookieValue(HttpServletRequest request, String cookie1NameToken) {//COOKIE1_NAME_TOKEN-->"token"
        //遍历request里面所有的cookie
        Cookie[] cookies=request.getCookies();
        if(cookies!=null) {
            for(Cookie cookie :cookies) {
                if(cookie.getName().equals(cookie1NameToken)) {
                    System.out.println("getCookieValue:"+cookie.getValue());
                    return cookie.getValue();
                }
            }
        }
        System.out.println("No getCookieValue!");
        return null;
    }
}

4.6 限制用户访问次数

  • 在拦截器拦截方法执行前,去执行相关注解的逻辑,此处是对用户访问次数的访问
  • 取到注解中的参数,在指定的时间内进行判断
  • 首先判断用户的访问次数是否为null
  • 是,则意味着用户之前未访问过,则设置count为1
  • 接着判断访问的次数是否超过指定的次数,没有,访问次数加1,否则限制访问
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值