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初探外篇]__关于正则表达式
正则表达式通常用于判断语句之中,用来检测一段字符串是否满足某一个格式.在日常生活中被广泛的用于各种用户输入信息的检测上. 而正则表达式实际上是一些具有特殊意义的字符序列.通过这些特殊字符构成的特殊序列 ...
随机推荐
- Idea Live Temlpates 自定义代码
Idea Live Temlpates 自定义代码 目的 - 加快常用代码的书写 使用## 打开Settings 快捷键Ctrl+Alt+S 选中Live Templates 选中temlpates ...
- VSCode C++环境配置(个人使用)
tasks.json { "version": "2.0.0", "command": "g++", "arg ...
- 全球疫情爬取APP版
全球疫情统计APP图表展示: 将该任务分解成三部分来逐个实现: ①爬取全球的疫情数据存储到云服务器的MySQL上 ②在web项目里添加一个servlet,通过参数的传递得到对应的json数据 ③设计A ...
- 介绍一种 Python 更方便的爬虫代理池实现方案
现在搞爬虫,代理是不可或缺的资源 很多人学习python,不知道从何学起.很多人学习python,掌握了基本语法过后,不知道在哪里寻找案例上手.很多已经做案例的人,却不知道如何去学习更加高深的知识.那 ...
- Iconfont的代码使用
1.Iconfont官网 相关阅读: Iconfont-阿里巴巴矢量图标库 Iconfont-阿里巴巴矢量图标库-代码使用 2.下载代码 注意到把鼠标悬停到图标上,会出现三个按钮. 我们点击" ...
- SQL关联查询
从2张或多张表中,取出有关联的数据 关联查询一共有几种情况: 内连接:INNER JOIN .CROSS JOIN (1)形式一 select 字段列表 from A表 inner join B表 o ...
- arcgis api for js 之网络分析服务发布
1.引言 百度地图上有这样的功能:点击两个点,地图上会显示对两个点的路径规划.这个功能能否利用 arcgis api 实现呢?答案是肯定的.不过在实现之前,我们需要将数据发布为网络分析服务,接下来我将 ...
- LeetCode 861翻转矩阵后得分详细解法
1. 题目内容 有一个二维矩阵 A 其中每个元素的值为 0 或 1 . 移动是指选择任一行或列,并转换该行或列中的每一个值:将所有 0 都更改为 1,将所有 1 都更改为 0. 在做出任意次数的移动后 ...
- vue.extend和vue.component的区别
vue.extend 使用基础 Vue 构造器函数,通过原型继承,(返回)创建一个"子类"(构造器).参数是一个包含组件选项的对象. const Sub = function Vu ...
- python - 基础局部变量和全局变量
python中全局变量和局部变量的最大区别在于局部变量只能通过函数去访问,而全局变量可以直接访问 首先我们来看下什么是全局变量和局部变量 全局变量:在函数之外定义的变量,所有函数内可以调用这个全局变量 ...