引言

当我们秒杀开始时,不会直接调秒杀接口,而是获取真正秒杀接口的地址,根据每个用户秒杀的不同商品是不一样的。这样可以避免有些人提前通过脚本准备好固定地址进行秒杀。这种方式的缺点是有可能能提前获取到秒杀接口地址,这种时候可以再进行一次验证码的防护。如果没有验证码的话,一秒内可能有很多请求,加上验证码可以延迟请求的时间,服务器承受的压力就没有那么大。为了减少并发量,还可以进行一次接口的限流。

一、秒杀接口地址隐藏

针对不同用户秒杀不同商品,设计秒杀接口地址不同。

1.1 控制层修改

/*** 秒杀* @author 47roro* @date 2022/4/16* @param path* @param user* @param goodsId* @return java.lang.String**/
@RequestMapping(value = "/{path}/doSeckill", method = RequestMethod.POST)
@ResponseBody
public RespBean doSecKill(@PathVariable String path, User user, Long goodsId){if(user == null){return RespBean.error(RespBeanEnum.SESSION_ERROR);}ValueOperations valueOperations = redisTemplate.opsForValue();//判断路径是否正确Boolean check = orderService.checkPath(user, goodsId, path);if(!check){return RespBean.error(RespBeanEnum.REQUEST_ILLEGAL);}//判断是否重复抢购(mybatis plus)SeckillOrder seckillOrder = (SeckillOrder) redisTemplate.opsForValue().get("order:" + user.getId() + ":" + goodsId);if(seckillOrder != null){return RespBean.error(RespBeanEnum.REPEAT_ERROR);}//内存标记减少redis访问if(EmptyStockMap.get(goodsId)){return RespBean.error(RespBeanEnum.EMPT_STOCK);}//预减库存Long stock = valueOperations.decrement("seckillGoods:" + goodsId);//Long stock = (Long) redisTemplate.execute(script,//        Collections.singletonList("seckillGoods:" + goodsId),//        Collections.EMPTY_LIST);if(stock < 0){EmptyStockMap.put(goodsId, true);valueOperations.increment("seckillGoods:" + goodsId);return RespBean.error(RespBeanEnum.EMPT_STOCK);}SeckillMessage seckillMessage = new SeckillMessage(user, goodsId);mqSender.sendSeckillMessage(JsonUtil.object2JsonStr(seckillMessage));return RespBean.success(0);
}
/*** 获取秒杀地址* @author 47roro* @date 2022/5/13* @param user* @param goodsId* @return com.example.seckill.vo.RespBean**/
@RequestMapping(value = "/path", method = RequestMethod.GET)
@ResponseBody
public RespBean getPath(User user, Long goodsId) {if (user == null) {return RespBean.error(RespBeanEnum.SESSION_ERROR);}String str = orderService.createPath(user, goodsId);return RespBean.success(str);
}

1.2 订单服务接口修改

/*** 获取秒杀地址* @author 47roro* @date 2022/5/13* @param user* @param goodsId* @return java.lang.String**/
String createPath(User user, Long goodsId);/*** 校验秒杀地址* @author 47roro* @date 2022/5/13* @param user* @param goodsId* @param path* @return java.lang.Boolean**/
Boolean checkPath(User user, Long goodsId, String path);

1.3 订单服务修改

/*** 获取秒杀地址* @author 47roro* @date 2022/5/13* @param user* @param goodsId* @return java.lang.String**/
@Override
public String createPath(User user, Long goodsId) {String str = MD5Util.md5(UUIDUtil.uuid() + "123456");redisTemplate.opsForValue().set("seckillPath:" + user.getId() + ":" +goodsId, str, 60, TimeUnit.SECONDS);return str;
}/*** 校验秒杀地址* @author 47roro* @date 2022/5/13* @param user* @param goodsId* @param path* @return java.lang.Boolean**/
@Override
public Boolean checkPath(User user, Long goodsId, String path) {if (user==null|| !StringUtils.hasLength(path)){return false;}String redisPath = (String) redisTemplate.opsForValue().get("seckillPath:" +user.getId() + ":" + goodsId);return path.equals(redisPath);
}

1.4 前端页面修改

 function getSeckillPath(){var goodsId = $("#goodsId").val();g_showLoading();$.ajax({url: "/seckill/path",type: "GET",data: {goodsId: goodsId,},success: function (data) {if (data.code == 200) {var path = data.obj;doSeckill(path);} else {layer.msg(data.message);}},error: function () {layer.msg("客户端请求错误");}})}

1.5 结果测试

获取到唯一path,与redis中存储的一致。

1.6 小结

这种方式还存在一种缺点,就是有些人可以通过获取到一次地址后,能立马获取拼接规则,如果知道了拼接规则的话,可以快速发起大量请求。这种时候可以通过加上验证码进行限制。脚本不会进行验证码的校验。能够隔离掉一部分的脚本请求。

二、 生成图形验证码

验证码作用:

  • 防止一部分脚本;
  • 拉长短时间并发的时间长度。

最好避免简单验证码。可以用数学公式,图形翻转等。验证码可以使用开源的项目。
点击秒杀开始前,先输入验证码,分散用户请求。

2.1 前端页面修改

<div class="row"><div class="form-inline"><img id="captchaImg" width="130" height="32" onclick="refreshCaptcha()"style="display: none"><input id="captcha" class="form-control" style="display: none"><button class="btn btn-primary" type="button" id="buyButton"onclick="getSeckillPath()">立即秒杀<input type="hidden" name="goodsId" id="goodsId"></button></div>
</div>
function refreshCaptcha(){$("#captchaImg").attr("src", "/seckill/captcha?goodsId=" + $("#goodsId").val() + "&time=" + new Date());}
function countDown() {var remainSeconds = $("#remainSeconds").val();var timeout;//秒杀还未开始if (remainSeconds > 0) {$("#buyButton").attr("disabled", true);$("#seckillTip").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);}$("#seckillTip").html("秒杀进行中");$("#captchaImg").attr("src", "/seckill/captcha?goodsId=" + $("#goodsId").val() + "&time=" + new Date());$("#captchaImg").show();$("#captcha").show();} else {$("#buyButton").attr("disabled", true);$("#seckillTip").html("秒杀已经结束");$("#captchaImg").hide();$("#captcha").hide();}
}

2.2 控制层修改

/*** 生成验证码* @author 47roro* @date 2022/5/13* @param user* @param goodsId* @param response**/
@RequestMapping(value = "/captcha", method = RequestMethod.GET)
public void verifyCode(User user, Long goodsId, HttpServletResponse response) {if (null==user||goodsId<0){throw new GlobalException(RespBeanEnum.REQUEST_ILLEGAL);}// 设置请求头为输出图片类型response.setContentType("image/jpg");response.setHeader("Pragma", "No-cache");response.setHeader("Cache-Control", "no-cache");response.setDateHeader("Expires", 0);//生成验证码,将结果放入redisArithmeticCaptcha captcha = new ArithmeticCaptcha(130, 32, 3);redisTemplate.opsForValue().set("captcha:"+user.getId()+":"+goodsId,captcha.text(),300, TimeUnit.SECONDS);try {captcha.out(response.getOutputStream());} catch (IOException e) {log.error("验证码生成失败", e.getMessage());}
}

2.3 测试结果

三、校验验证码

3.1 前端修改

添加验证码的传输

3.2 控制层修改

进行验证码校验

/*** 获取秒杀地址* @author 47roro* @date 2022/5/13* @param user* @param goodsId* @return com.example.seckill.vo.RespBean**/
@RequestMapping(value = "/path", method = RequestMethod.GET)
@ResponseBody
public RespBean getPath(User user, Long goodsId, String captcha) {if (user == null) {return RespBean.error(RespBeanEnum.SESSION_ERROR);}boolean check = orderService.checkCaptcha(user, goodsId, captcha);if(!check){return RespBean.error(RespBeanEnum.ERROR_CAPTCHA);}String str = orderService.createPath(user, goodsId);return RespBean.success(str);
}

3.3 接口及实现类修改

实现类:

/*** 校验验证码* @author 47roro* @date 2022/5/13* @param user* @param goodsId* @param captcha* @return boolean**/
@Override
public boolean checkCaptcha(User user, Long goodsId, String captcha) {if(!StringUtils.hasLength(captcha) || user == null || goodsId < 0){return false;}String redisCaptcha = (String) redisTemplate.opsForValue().get("captcha:" + user.getId() + ":" + goodsId);return captcha.equals(redisCaptcha);
}

接口:

/*** 验证码校验* @author 47roro* @date 2022/5/13* @param user* @param goodsId* @param captcha* @return boolean**/
boolean checkCaptcha(User user, Long goodsId, String captcha);

3.4 结果测试

输入错误答案:

输入正确答案:

四、接口限流

通过限流可以控制系统的QPS,减小服务器的压力。

通用接口限流

4.1 用户环境类

将用户保存在ThreadLocal中,

/*** @author 47roro* @create 2022/5/13* @description:*/
public class UserContext {private static ThreadLocal<User> userHolder = new ThreadLocal<User>();public static void setUser(User user) {userHolder.set(user);}public static User getUser() {return userHolder.get();}
}

4.2 用户解析修改

从threadlocal中获取用户

@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {return UserContext.getUser();
}

4.3 配置登录拦截器

/*** @author 47roro* @create 2022/5/13* @description: 注解拦截器*/
@Component
public class AccessLimitInterceptor implements HandlerInterceptor {@Autowiredprivate IUserService userService;@Autowiredprivate RedisTemplate redisTemplate;@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {if (handler instanceof HandlerMethod) {User user = getUser(request, response);UserContext.setUser(user);HandlerMethod hm = (HandlerMethod) handler;AccessLimit accessLimit = hm.getMethodAnnotation(AccessLimit.class);if (accessLimit == null) {return true;}int second = accessLimit.second();int maxCount = accessLimit.maxCount();boolean needLogin = accessLimit.needLogin();String key = request.getRequestURI();if (needLogin) {if (user == null) {render(response, RespBeanEnum.SESSION_ERROR);return false;}key += ":" + user.getId();}ValueOperations valueOperations = redisTemplate.opsForValue();Integer count = (Integer) valueOperations.get(key);if (count == null) {valueOperations.set(key, 1, second, TimeUnit.SECONDS);} else if (count < maxCount) {valueOperations.increment(key);} else {render(response, RespBeanEnum.ACCESS_LIMIT_REACHED);return false;}}return true;}/*** 构建返回对象* @author 47roro* @date 2022/5/13* @param response* @param respBeanEnum**/private void render(HttpServletResponse response, RespBeanEnum respBeanEnum) throws IOException {response.setContentType("application/json");response.setCharacterEncoding("UTF-8");PrintWriter out = response.getWriter();RespBean respBean = RespBean.error(respBeanEnum);out.write(new ObjectMapper().writeValueAsString(respBean));out.flush();out.close();}/*** 获取当前登录用户* @author 47roro* @date 2022/5/13* @param request* @param response* @return com.example.seckill.pojo.User**/private User getUser(HttpServletRequest request, HttpServletResponse response) {String cookie = CookieUtil.getCookieValue(request, "userCookie");if (!StringUtils.hasLength(cookie)) {return null;}return userService.getUserByCookie(cookie, request, response);}
}

自定义注解:

/*** @author 47roro* @create 2022/5/13* @description: 访问限制注解*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AccessLimit {int second();int maxCount();boolean needLogin() default true;
}

4.4 MVC配置修改

将登录拦截器添加进MVC配置

@Override
public void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(accessInterceptor);
}

4.5 秒杀控制器注解

在秒杀控制器上添加登录拦截注解
@AccessLimit(second = 5, maxCount = 5, needLogin = true)
被拦截后进入拦截器判断是否频繁登录

4.6 结果测试

【学习笔记】seckill-秒杀项目--(10)安全优化相关推荐

  1. 秒杀项目05-页面优化技术

    秒杀项目05-页面优化技术 1. 页面缓存+URL缓存+对象缓存 1.1 页面缓存 1.2 URL缓存 1.3 对象缓存(更细粒度的缓存) 2. 页面静态化,前后端分离 1. 常用技术AngularJ ...

  2. 【全栈之巅】Node.js + Vue.js 全栈开发王者荣耀手机端官网和管理后台学习笔记(4.1-4.10)

    [全栈之巅]Node.js + Vue.js 全栈开发王者荣耀手机端官网和管理后台学习笔记(4.1-4.10) 本项目是 学习Bilibili 全栈之巅 视频教程相关源码和体会 https://git ...

  3. 【全栈之巅】Node.js + Vue.js 全栈开发王者荣耀手机端官网和管理后台学习笔记(3.6-3.10)

    [全栈之巅]Node.js + Vue.js 全栈开发王者荣耀手机端官网和管理后台学习笔记(3.6-3.10) 本项目是 学习Bilibili 全栈之巅 视频教程相关源码和体会 https://git ...

  4. Scrapy:学习笔记(2)——Scrapy项目

    Scrapy:学习笔记(2)--Scrapy项目 1.创建项目 创建一个Scrapy项目,并将其命名为"demo" scrapy startproject demo cd demo ...

  5. STM32学习笔记——MDK新建项目

    STM32学习笔记 KIEL新建项目过程 文章目录 STM32学习笔记 前言 一.keil新建库函数版本项目 二.注意事项 1.文件分类 2.MDK配置 总结 前言 此处使用寄存器版,进行学习.后期看 ...

  6. DSB2017项目grt123代码学习笔记一:项目基本情况

    DSB2017项目grt123代码学习笔记一:项目基本情况 Kaggle上Data Science Bowl 2017年肺结节检测比赛第一名grt123团队的算法. github地址:https:// ...

  7. PyTorch学习笔记2:nn.Module、优化器、模型的保存和加载、TensorBoard

    文章目录 一.nn.Module 1.1 nn.Module的调用 1.2 线性回归的实现 二.损失函数 三.优化器 3.1.1 SGD优化器 3.1.2 Adagrad优化器 3.2 分层学习率 3 ...

  8. CV学习笔记 | CV综述 [2020.10.01]

    文章目录 0. 概述(整理完后随时修改) 1. 人工神经网络 1.1. 人工神经网络发展历程 1.2. 一些神经元节点的工作原理 1.2.1. 基本神经元 1.2.2. 卷积神经元(Convoluti ...

  9. 强化学习笔记:PPO 【近端策略优化(Proximal Policy Optimization)】

    1 前言 我们回顾一下policy network: 强化学习笔记:Policy-based Approach_UQI-LIUWJ的博客-CSDN博客 它先去跟环境互动,搜集很多的 路径τ.根据它搜集 ...

  10. CI/CD学习笔记-Jenkins综合项目Github+Jenkins+Harbor+Docker

    实验说明 实验拓扑 部署流程 研发push到github代码库 Jenkins 构建,pull git代码 使用maven进行编译打包 打包生成的代码,生成一个新版本的镜像,push到本地docker ...

最新文章

  1. 图像拼接--Construction and Refinement of Panoramic Mosaics with Global and Local Alignment
  2. 如何用ChemFinder制作子表单
  3. Deepin安装最新显卡RTX2080Ti及CUDA10.1
  4. KVO 实现两个页面之间的通信
  5. android studio上拉加载,AndroidStudio的PullToRefreshListView简单使用
  6. Linux开发标准LSB简介:Linux Standard Base
  7. OpenCV学习笔记:基础结构
  8. c程序100例第3题
  9. ios 读取各种类型文件
  10. panda是删除行_如何从Pandas数据帧中删除行列表?
  11. 12 个顶级 Bug 跟踪工具
  12. asp.net跳转页面的三种方法比较
  13. 每天一道剑指offer-包含min函数的栈
  14. typora插入文件到服务器,写作神器Typora入门指南
  15. 电脑上最好的5个azw3阅读器
  16. usb无线网卡断线后找不到网络
  17. 在线qq客服的html代码生成器,js生成qq客服在线代码
  18. 软件测试 因果+决策案例--中国象棋中走马
  19. 电池SOC仿真系列-基于遗传算法的电池参数辨识
  20. STM32单片机的学习方法(方法大体适用所有开发版入门)

热门文章

  1. html打印纸张尺寸设置100*150,16开纸是多大?打印机如何设置16开纸张?
  2. 苹果鼠标右键怎么按_IOS13.4更新后的鼠标用途在哪?
  3. ping一下网站服务器的域名,怎么PING一个网站的域名
  4. 个人总结 onSaveInstanceState方法会在什么时候被执行
  5. SQL实现次日、三日及七日用户留存率的计算
  6. 壬戌立冬十朋十一日陪路文周朋携提祖同访后 [宋] 王洋
  7. win10照片查看器_Windows 10如何找回自带的照片查看器?
  8. 禁用笔记本电脑自带键盘
  9. 论文写作学习心得体会
  10. 国际信用卡VISA/MasterCard/AE/DC/JCB 卡号结构