java初探(1)之秒杀的安全
在秒杀的场景中还存在着很多的安全问题
- 暴露秒杀地址
- 秒杀请求可以很频繁
- 接口流量大,恶意刷接口
隐藏秒杀接口
为什么需要隐藏,事实上,页面上的所有东西都能被客户端拿到,包括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)之秒杀的安全的更多相关文章
- [java初探总结篇]__java初探总结
前言 终于,java初探系列的学习,要告一阶段了,java初探系列在我的计划中是从头学java中的第一个阶段,知识主要涉及java的基础知识,所以在笔记上实在花了不少的功夫.虽然是在第一阶段上面花费了 ...
- [java初探10]__关于数字处理类
前言 在我们的日常开发过程中,我们会经常性的使用到数字类型的数据,同时,也会有众多的对数字处理的需求,针对这个方面的问题,在JAVA语言中.提供解决方法的类就是数字处理类 java中的数字处理类包括: ...
- java初探(1)之秒杀项目总结
在开始总结之前,先记录一个刚看到的博客,编程规约.该博客记录了一些java开发上的规范,可以在编码的时候引入这些规范. 无论流行框架一直怎么改变,web开发中的三层架构一直属于理论的基础存在. 表现层 ...
- java初探(1)之秒杀中的rabbitMQ
rabbitMQ 消息队列,通过一定的通信协议,生产者和消费者在应用程序内传递通信. 主要的作用,提高负载,减耦合. 场景描述:当点击秒杀按钮的那个时刻,有很高的并发量,客户端发出请求之后,会判断库存 ...
- java初探(1)之秒杀的业务简单实现
前言 秒杀的业务场景广泛存在于电商当中,即有一个倒计时的时间限制,当倒计时为0时,秒杀开始,秒杀之后持续很小的一段时间,而且秒杀的商品很少,因此会有大量的顾客进行购买,会产生很大的并发量,从而创造技术 ...
- java初探native
最近碰见一个java中一个native关键字,不知道是干什么的,如下: public native String FileName(String strURL); static{ ...
- java初探(1)之登录总结
登录总结 前几章总结了登录各个步骤中遇到的问题,现在完成的做一个登录的案例,其难点不在于实现功能,而在于抽象各种功能模块,提高复用性,较低耦合度. 前端页面: 对于前端页面来说,不是后端程序员要考虑的 ...
- java初探/java读取文件
import java.io.*; import java.util.Arrays; public class WriteText { public static void main(String[] ...
- [Java初探外篇]__关于正则表达式
正则表达式通常用于判断语句之中,用来检测一段字符串是否满足某一个格式.在日常生活中被广泛的用于各种用户输入信息的检测上. 而正则表达式实际上是一些具有特殊意义的字符序列.通过这些特殊字符构成的特殊序列 ...
随机推荐
- [vue] computed 和 method
计算属性 计算属性只有在它的相关依赖发生改变时才会重新取值 Method method每次渲染的时候都会被执行 举一个栗子 <template>...<div> <p& ...
- C语言学习笔记之switch有无break差别
今天复习到了switch语句,我们正常的写法中,是经常把switch和break搭配在一起的 当我们把break去掉呢 可以看到,我们选择2时,的确会到case 2里面执行语句,可是没有break还是 ...
- JS 导航条切换案例
HTML代码: <div class="tab"> <div class="tab_list"> <ul> <li c ...
- Windows下使用命令行编译Qt项目(解决DLL丢失问题)
一.前言 我之前用Qt做了个hello world,结果各种报错,一大堆DLL找不到,今天用命令行编译就通过了 二.准备工作 1.Visual Studio(有nmake就行) 2.Qt 3.把qma ...
- canvas图片编辑操作:缩放、移动、保存(PC端+移动端)
最近在写canvas关于图片的操作,看了网上的代码基本都是不行的,于是就自己写了一个. html代码 <canvas id="myCanvas" width="37 ...
- C#设计模式之17-中介者模式
中介者模式(Mediator Pattern) 该文章的最新版本已迁移至个人博客[比特飞],单击链接 https://www.byteflying.com/archives/419 访问. 中介者模式 ...
- Vue 使用v-for对Object进行遍历
v-for 也可以对Object类型数据进行遍历 value在前, key在后 <div v-for="(value,key) in person"> <p> ...
- golang 的 string包
前言 不做文字搬运工,多做思路整理 就是为了能速览标准库,只整理我自己看过的...... 注意!!!!!!!!!! 单词都是连着的,我是为了看着方便.理解方便才分开的 1.string 中文文档 [英 ...
- 通过实际案例摸清楚Spring事务传播的行为
@ 目录 事务传播 案例准备 案例解析 1.无事务 2. Propagation.REQUIRED 3. Propagation.SUPPORTS 4. Propagation.MANDATORY 5 ...
- Jmeter 常用函数(26)- 详解 __chooseRandom
如果你想查看更多 Jmeter 常用函数可以在这篇文章找找哦 https://www.cnblogs.com/poloyy/p/13291704.html 作用 从指定的范围里面取值 语法格式 ${_ ...