栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

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

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

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

引言

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

一、秒杀接口地址隐藏

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

1.1 控制层修改
@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);
}

@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 订单服务接口修改
String createPath(User user, Long goodsId);


Boolean checkPath(User user, Long goodsId, String path);
1.3 订单服务修改
@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;
}


@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 前端页面修改
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 控制层修改
@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);
    //生成验证码,将结果放入redis
    ArithmeticCaptcha 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 控制层修改

进行验证码校验

@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 接口及实现类修改

实现类:

@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);
}

接口:

boolean checkCaptcha(User user, Long goodsId, String captcha);
3.4 结果测试

输入错误答案:

输入正确答案:

四、接口限流

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

通用接口限流

4.1 用户环境类

将用户保存在ThreadLocal中,

public class UserContext {

	private static ThreadLocal userHolder = new ThreadLocal();

	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 配置登录拦截器
@Component
public class AccessLimitInterceptor implements HandlerInterceptor {

	@Autowired
	private IUserService userService;
	@Autowired
	private RedisTemplate redisTemplate;

	@Override
	public 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;
	}


	
	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();
	}

	
	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);
	}
}

自定义注解:

@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 结果测试

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/881572.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号