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

  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);
}
将拦截器生效以后,就可以使用注解来设置防盗刷了。 

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

  1. [java初探总结篇]__java初探总结

    前言 终于,java初探系列的学习,要告一阶段了,java初探系列在我的计划中是从头学java中的第一个阶段,知识主要涉及java的基础知识,所以在笔记上实在花了不少的功夫.虽然是在第一阶段上面花费了 ...

  2. [java初探10]__关于数字处理类

    前言 在我们的日常开发过程中,我们会经常性的使用到数字类型的数据,同时,也会有众多的对数字处理的需求,针对这个方面的问题,在JAVA语言中.提供解决方法的类就是数字处理类 java中的数字处理类包括: ...

  3. java初探(1)之秒杀项目总结

    在开始总结之前,先记录一个刚看到的博客,编程规约.该博客记录了一些java开发上的规范,可以在编码的时候引入这些规范. 无论流行框架一直怎么改变,web开发中的三层架构一直属于理论的基础存在. 表现层 ...

  4. java初探(1)之秒杀中的rabbitMQ

    rabbitMQ 消息队列,通过一定的通信协议,生产者和消费者在应用程序内传递通信. 主要的作用,提高负载,减耦合. 场景描述:当点击秒杀按钮的那个时刻,有很高的并发量,客户端发出请求之后,会判断库存 ...

  5. java初探(1)之秒杀的业务简单实现

    前言 秒杀的业务场景广泛存在于电商当中,即有一个倒计时的时间限制,当倒计时为0时,秒杀开始,秒杀之后持续很小的一段时间,而且秒杀的商品很少,因此会有大量的顾客进行购买,会产生很大的并发量,从而创造技术 ...

  6. java初探native

    最近碰见一个java中一个native关键字,不知道是干什么的,如下: public native String FileName(String strURL);     static{        ...

  7. java初探(1)之登录总结

    登录总结 前几章总结了登录各个步骤中遇到的问题,现在完成的做一个登录的案例,其难点不在于实现功能,而在于抽象各种功能模块,提高复用性,较低耦合度. 前端页面: 对于前端页面来说,不是后端程序员要考虑的 ...

  8. java初探/java读取文件

    import java.io.*; import java.util.Arrays; public class WriteText { public static void main(String[] ...

  9. [Java初探外篇]__关于正则表达式

    正则表达式通常用于判断语句之中,用来检测一段字符串是否满足某一个格式.在日常生活中被广泛的用于各种用户输入信息的检测上. 而正则表达式实际上是一些具有特殊意义的字符序列.通过这些特殊字符构成的特殊序列 ...

随机推荐

  1. 03-Thread类中的常用方法

    Thread类中的常用的方法: * 1. start():启动当前线程:调用当前线程的run() * 2. run(): 通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明在此方法中 ...

  2. linux下的node版本管理利器:nvm

    nvm是一款node版本管理工具,简单来说,如果你想在一个环境下安装多个node版本,并向自由地切换相关版本,那你就需要使用nvm进行版本管理,有点类似pyenv,也是一款python版本管理工具. ...

  3. SourceTreet提交时显示remote: Incorrect username or password ( access token )(4种解决办法)

    引言 我因为第一次安装Sources Tree的时候进行破解时(跳过安装时的登录),因为操作失误造成了好多bug,导致Sources Tree不论提交,拉取,获取,都会报remote: Incorre ...

  4. 基于.NetCore3.1系列 —— 日志记录之自定义日志组件

    一.前言 回顾:日志记录之日志核心要素揭秘 在上一篇中,我们通过学习了解在.net core 中内置的日志记录中的几大核心要素,在日志工厂记录器(ILoggerFactory)中实现将日志记录提供器( ...

  5. Provisional headers are shown 问题的一种情况

    Provisional headers are shown 出现在请求头的报错里面,意思为 显示临时的头部,真实的意思是,请求没有收到服务器返回.如果出现类似情况, 可以在服务端找一找,是否没有给该请 ...

  6. Name jms can't bind to context问题解决

    需要把gis-datamanage包中的配置test改成compile

  7. SpringSecurity权限管理系统实战—六、SpringSecurity整合jwt

    目录 SpringSecurity权限管理系统实战-一.项目简介和开发环境准备 SpringSecurity权限管理系统实战-二.日志.接口文档等实现 SpringSecurity权限管理系统实战-三 ...

  8. C++炮台实验

    炮台实验 蒜头君在玩一个战争模拟游戏,他有高度为 1,2,3,... ,n的炮台各一个,他需要把这 n个炮台从左往右排成一行,并且炮口都朝向右边. 在这个游戏中,所有炮台发射的炮弹会摧毁前方所有高度比 ...

  9. java泛型笔记

    目录 概述 什么是泛型?为什么使用泛型? 例子 特性 使用方式 泛型类 泛型接口 泛型通配符 泛型方法 泛型方法的基本用法 类中的泛型方法 泛型方法的基本用法 泛型方法与可变参数 静态方法与泛型 泛型 ...

  10. 冒泡排序(Bubble Sorting)

    基本介绍 时间复杂度O(n^2) 冒泡排序(Bubble Sorting)的基本思想是:通过对待 排序序列从前向后(从下标较小的元素开始),依次比较 相邻元素的值,若发现逆序则交换,使值较大 的元素逐 ...