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

  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. spring security 简介+实战

    过滤器链: 依赖: security 功能列表: 一.登录验证.权限验证 1.1 httpbasic验证 1.2form验证 建立数据需要遵循RBAC模型 用户表要参考UserDetail创建 实例类 ...

  2. DevOps让金融业数字化转型更敏捷 | 分享实录

    以下为博云近期在活动中分享的关于<如何通过 DevOps 让数字化转型变得更加敏捷>的主题演讲实录. 01 金融科技进入VUCA时代 大家好,今天分享的题目是<如何通过 DevOps ...

  3. 面试这么撩准拿offer,HashMap深度学习,扰动函数、负载因子、扩容拆分,原理和实践验证,让懂了就是真的懂!

    作者:小傅哥 博客:https://bugstack.cn 沉淀.分享.成长,让自己和他人都能有所收获! 一.前言 得益于Doug Lea老爷子的操刀,让HashMap成为使用和面试最频繁的API,没 ...

  4. HTML学习第三天

    超链接: <a href=""></a>             target链接打开方式 1._blank新窗口打开 2._self当前窗口打开     ...

  5. 2020-04-14:mysql原子性和持久性怎么保证

    1.Mysql怎么保证一致性的? OK,这个问题分为两个层面来说. 从数据库层面,数据库通过原子性.隔离性.持久性来保证一致性.也就是说ACID四大特性之中,C(一致性)是目的,A(原子性).I(隔离 ...

  6. 45道Promise面试题

    来看看通过阅读本篇文章要点: Promise的几道基础题 Promise结合setTimeout Promise中的then.catch.finally Promise中的all和race async ...

  7. 因网络时代与云端应用而生的AGPL-3.0授权条款

    ​ 此篇文章转载自:因應網路時代與雲端應用而生的 AGPL-3.0 授權條款 如你所见,原文为繁体,我将其转为简体并将"网路"替换为"网络",方便阅读.并未修改 ...

  8. generate_fixed_frame()方法生成Java方法栈帧

    在从generate_normal_entry()函数调用generate_fixed_frame()函数时的栈与寄存器的状态如下: 栈的状态如下图所示. 各个寄存器的状态如下所示. rax: ret ...

  9. C++ U型数

    U型数字 最近蒜头君喜欢上了U型数字,所谓U型数字,就是这个数字的每一位先严格单调递减,后严格单调递增.比如 212212 就是一个U型数字,但是 333333, 9898, 567567, 3131 ...

  10. 使用openpyxl创建excel,设置不显示网格线

    最近在学openpyxl,想设置excel不显示网格线,试了好多种方法都不行,最后发现可以通过修改views文件来实现. 文件路径:虚拟目录\Lib\site-packages\openpyxl\wo ...