滑动验证码的设计与理解

时间:2021-09-14 15:21:57

在介绍之前,首先一个概念明确一个共识:没有攻不破的网站,只有值不值得。

这意思是说,我们可以尽可能的提高自己网站的安全,但并没有绝对的安全,当网站安全级别大于攻击者能得到的回报时,你的网站就是安全的。

所以百度搜到的很多验证码都已经结合了人工智能分析用户行为,很厉害。但这里只介绍我的小网站是怎么设计的。

大概逻辑:当需要验证码时,前端发送ajax向后台请求相关数据发送回前端,由前端生成(与后端生成图片,然后传送图片到前端的做法相比安全性要差很多。但也是可以预防的,后端可以对此Session进行请求记录,如果在一定时间内恶意多次请求,可以进行封禁ip等对策),验证完成后,后台再对传回的数据进行校验。

效果图:

滑动验证码的设计与理解

滑动验证码的设计与理解

1|0js类的设计:

 

1.定义一个验证码父类,因为目前只有这一个验证类型,倘若以后再要扩展其他验证类型呢。那么它们之间肯定有很多公共之处(如:验证成功、失败的回调,获取验证码的类型,获取验证结果等),所以这些共同点可以提炼出来,下面是我目前的父类样子:

 

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
/**
* 验证码的父类,所有验证码都要继承这个类
* @param id 验证码的唯一标识
* @param type 验证码的类型
* @param contentDiv 包含着验证码的DIV
* @constructor
*/
var Identifying = function (id,type,contentDiv){
 this.id = id;
 this.type = type;
 this.contentDiv=contentDiv;
}
/**
* 销毁函数
*/
Identifying.prototype.destroy = function(){
 this.successFunc = null;
 this.errorFunc = null;
 this.clearDom();
 this.contentDiv = null;
}
/**
* 清除节点内容
*/
Identifying.prototype.clearDom = function(){
 if(this.contentDiv instanceof jQuery){
  this.contentDiv.empty();
 }else if(this.contentDiv instanceof HTMLElement){
  this.contentDiv.innerText = "";
 }
}
/**
* 回调函数
* 验证成功后进行调用
* this需要指具体验证类
* @param result 对象,有对应验证类的传递的参数,具体要看验证类
*/
Identifying.prototype.success = function (result) {
 if(this.successFunc instanceof Function){
  this.successFunc(result);
 }
}
/**
* 验证失败发生错误调用的函数
* @param result
*/
Identifying.prototype.error = function (result) {
 if(this.errorFunc instanceof Function){
  this.errorFunc(result);
 }else{
  //统一处理错误
 }
}
/**
* 获取验证码id
*/
Identifying.prototype.getId = function () {
 return this.id;
}
/**
* 获取验证码类型
* @returns {*}
*/
Identifying.prototype.getType = function () {
 return this.type;
}
/**
* 显示验证框
*/
Identifying.prototype.showIdentifying = function(callback){
 this.contentDiv.show(null,callback);
}
/**
* 隐藏验证框
*/
Identifying.prototype.hiddenIdentifying = function(callback){
 this.contentDiv.hide(null,callback);
}
/**
* 获得验证码显示的dom元素
*/
Identifying.prototype.getContentDiv = function () {
 return this.contentDiv;
}

然后,滑动验证码类继承此父类(js继承会单独写篇文章),滑动验证码类如下:

  

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
/**
* 滑动验证类
* complete传递的参数为identifyingId,identifyingType,moveEnd_X
* @param config 各种配置
*/
var ImgIdentifying = function(config) {
 Identifying.call(this, config.identifyingId, config.identifyingType,config.el);
 this.config = config;
 this.init();
 this.showIdentifying();
}
//继承父类
extendClass(Identifying, ImgIdentifying);
/**
* 销毁函数
*/
ImgIdentifying.prototype.destroy = function () {
 Identifying.prototype.destroy.call(this);
}
var width = '260';
var height = '116';
var pl_size = 48;
var padding_ = 20;
ImgIdentifying.prototype.init = function () {
 this.clearDom();
 var el = this.getContentDiv();
 var w = width;
 var h = height;
 var PL_Size = pl_size;
 var padding = padding_;
 var self = this;
 //这个要转移到后台
 function RandomNum(Min, Max) {
  var Range = Max - Min;
  var Rand = Math.random();
  if (Math.round(Rand * Range) == 0) {
   return Min + 1;
  } else if (Math.round(Rand * Max) == Max) {
   return Max - 1;
  } else {
   var num = Min + Math.round(Rand * Range) - 1;
   return num;
  }
 }
 //确定图片
 var imgSrc = this.config.img;
 var X = this.config.X;
 var Y = this.config.Y;
 var left_Num = -X + 10;
 var html = '<div style="position:relative;padding:16px 16px 28px;border:1px solid #ddd;background:#f2ece1;border-radius:16px;">';
 html += '<div style="position:relative;overflow:hidden;width:' + w + 'px;">';
 html += '<div style="position:relative;width:' + w + 'px;height:' + h + 'px;">';
 html += '<img id="scream" src="' + imgSrc + '" style="width:' + w + 'px;height:' + h + 'px;">';
 html += '<canvas id="puzzleBox" width="' + w + '" height="' + h + '" style="position:absolute;left:0;top:0;z-index:222;"></canvas>';
 html += '</div>';
 html += '<div class="puzzle-lost-box" style="position:absolute;width:' + w + 'px;height:' + h + 'px;top:0;left:' + left_Num + 'px;z-index:11111;">';
 html += '<canvas id="puzzleShadow" width="' + w + '" height="' + h + '" style="position:absolute;left:0;top:0;z-index:222;"></canvas>';
 html += '<canvas id="puzzleLost" width="' + w + '" height="' + h + '" style="position:absolute;left:0;top:0;z-index:333;"></canvas>';
 html += '</div>';
 html += '<p class="ver-tips"></p>';
 html += '</div>';
 html += '<div class="re-btn"><a></a></div>';
 html += '</div>';
 html += '<br>';
 html += '<div style="position:relative;width:' + w + 'px;margin:auto;">';
 html += '<div style="border:1px solid #c3c3c3;border-radius:24px;background:#ece4dd;box-shadow:0 1px 1px rgba(12,10,10,0.2) inset;">';//inset 为内阴影
 html += '<p style="font-size:12px;color: #486c80;line-height:28px;margin:0;text-align:right;padding-right:22px;">按住左边滑块,拖动完成上方拼图</p>';
 html += '</div>';
 html += '<div class="slider-btn"></div>';
 html += '</div>';
 el.html(html);
 var d = PL_Size / 3;
 var c = document.getElementById("puzzleBox");
 //getContext获取该dom节点的canvas画布元素
 //---------------------------------这一块是图片*缺失的那一块--------------------------------------
 var ctx = c.getContext("2d");
 ctx.globalCompositeOperation = "xor";
 //设置阴影模糊级别
 ctx.shadowBlur = 10;
 //设置阴影的颜色
 ctx.shadowColor = "#fff";
 //设置阴影距离的水平距离
 ctx.shadowOffsetX = 3;
 //设置阴影距离的垂直距离
 ctx.shadowOffsetY = 3;
 //rgba第四个参数是透明度,前三个是三原色,跟rgb比就是多了第四个参数
 ctx.fillStyle = "rgba(0,0,0,0.8)";
 //beginPath() 方法开始一条路径,或重置当前的路径。
 //提示:请使用这些方法来创建路径:moveTo()、lineTo()、quadricCurveTo()、bezierCurveTo()、arcTo() 以及 arc()。
 ctx.beginPath();
 //指线条的宽度
 ctx.lineWidth = "1";
 //strokeStyle 属性设置或返回用于笔触的颜色、渐变或模式
 ctx.strokeStyle = "rgba(0,0,0,0)";
 //表示画笔移到(X,Y)位置,没画东西
 ctx.moveTo(X, Y);
 //画笔才开始移动到指定坐标,之间画一条直线
 ctx.lineTo(X + d, Y);
 //绘制一条贝塞尔曲线,一共四个点确定,开始点(没在参数里),和两个控制点(1和2参数结合,3和4参数结合),结束点(5和6参数结合)
 ctx.bezierCurveTo(X + d, Y - d, X + 2 * d, Y - d, X + 2 * d, Y);
 ctx.lineTo(X + 3 * d, Y);
 ctx.lineTo(X + 3 * d, Y + d);
 ctx.bezierCurveTo(X + 2 * d, Y + d, X + 2 * d, Y + 2 * d, X + 3 * d, Y + 2 * d);
 ctx.lineTo(X + 3 * d, Y + 3 * d);
 ctx.lineTo(X, Y + 3 * d);
 //必须和beginPath()成对出现
 ctx.closePath();
 //进行绘制
 ctx.stroke();
 //根据fillStyle进行填充
 ctx.fill();
 //---------------------------------这个为要移动的块------------------------------------------------
 var c_l = document.getElementById("puzzleLost");
 //---------------------------------这个为要移动的块增加阴影------------------------------------------------
 var c_s = document.getElementById("puzzleShadow");
 var ctx_l = c_l.getContext("2d");
 var ctx_s = c_s.getContext("2d");
 var img = new Image();
 img.src = imgSrc;
 img.onload = function () {
  //从原图片,进行设置处理再显示出来(其实就是设置你想显示图片的位置2和3参数,和框w高h)
  ctx_l.drawImage(img, 0, 0, w, h);
 }
 ctx_l.beginPath();
 ctx_l.strokeStyle = "rgba(0,0,0,0)";
 ctx_l.moveTo(X, Y);
 ctx_l.lineTo(X + d, Y);
 ctx_l.bezierCurveTo(X + d, Y - d, X + 2 * d, Y - d, X + 2 * d, Y);
 ctx_l.lineTo(X + 3 * d, Y);
 ctx_l.lineTo(X + 3 * d, Y + d);
 ctx_l.bezierCurveTo(X + 2 * d, Y + d, X + 2 * d, Y + 2 * d, X + 3 * d, Y + 2 * d);
 ctx_l.lineTo(X + 3 * d, Y + 3 * d);
 ctx_l.lineTo(X, Y + 3 * d);
 ctx_l.closePath();
 ctx_l.stroke();
 //带阴影,数字越高阴影越严重
 ctx_l.shadowBlur = 10;
 //阴影的颜色
 ctx_l.shadowColor = "black";
 // ctx_l.fill(); 其实加这句就能有阴影效果了,不知道为什么加多个图层
 //分割画布的块
 ctx_l.clip();
 ctx_s.beginPath();
 ctx_s.lineWidth = "1";
 ctx_s.strokeStyle = "rgba(0,0,0,0)";
 ctx_s.moveTo(X, Y);
 ctx_s.lineTo(X + d, Y);
 ctx_s.bezierCurveTo(X + d, Y - d, X + 2 * d, Y - d, X + 2 * d, Y);
 ctx_s.lineTo(X + 3 * d, Y);
 ctx_s.lineTo(X + 3 * d, Y + d);
 ctx_s.bezierCurveTo(X + 2 * d, Y + d, X + 2 * d, Y + 2 * d, X + 3 * d, Y + 2 * d);
 ctx_s.lineTo(X + 3 * d, Y + 3 * d);
 ctx_s.lineTo(X, Y + 3 * d);
 ctx_s.closePath();
 ctx_s.stroke();
 ctx_s.shadowBlur = 20;
 ctx_s.shadowColor = "black";
 ctx_s.fill();
 //开始时间
 var beginTime;
 //结束时间
 var endTime;
 var moveStart = '';
 $(".slider-btn").mousedown(function (e) {
  $(this).css({"background-position": "0 -216px"});
  moveStart = e.pageX;
  beginTime = new Date().valueOf();
 });
 onmousemove = function (e) {
  var e = e || window.event;
  var moveX = e.pageX;
  var d = moveX - moveStart;
  if (moveStart == '') {
  } else {
   if (d < 0 || d > (w - padding - PL_Size)) {
   } else {
    $(".slider-btn").css({"left": d + 'px', "transition": "inherit"});
    $("#puzzleLost").css({"left": d + 'px', "transition": "inherit"});
    $("#puzzleShadow").css({"left": d + 'px', "transition": "inherit"});
   }
  }
 };
 onmouseup = function (e) {
  var e = e || window.event;
  var moveEnd_X = e.pageX - moveStart;
  var ver_Num = X - 10;
  var deviation = self.config.deviation;
  var Min_left = ver_Num - deviation;
  var Max_left = ver_Num + deviation;
  if (moveStart == '') {
  } else {
   endTime = new Date().valueOf();
   if (Max_left > moveEnd_X && moveEnd_X > Min_left) {
    $(".ver-tips").html('<i style="background-position:-4px -1207px;"></i><span style="color:#42ca6b;">验证通过</span><span></span>');
    $(".ver-tips").addClass("slider-tips");
    $(".puzzle-lost-box").addClass("hidden");
    $("#puzzleBox").addClass("hidden");
    setTimeout(function () {
     $(".ver-tips").removeClass("slider-tips");
    }, 2000);
    self.success({
     'identifyingId': self.config.identifyingId, 'identifyingType': self.config.identifyingType,
     'moveEnd_X': moveEnd_X
    })
   } else {
    $(".ver-tips").html('<i style="background-position:-4px -1229px;"></i><span style="color:red;">验证失败:</span><span style="margin-left:4px;">拖动滑块将悬浮图像正确拼合</span>');
    $(".ver-tips").addClass("slider-tips");
    setTimeout(function () {
     $(".ver-tips").removeClass("slider-tips");
    }, 2000);
    self.error();
   }
  }
  //0.5指动画执行到结束一共经历的时间
  setTimeout(function () {
   $(".slider-btn").css({"left": '0', "transition": "left 0.5s"});
   $("#puzzleLost").css({"left": '0', "transition": "left 0.5s"});
   $("#puzzleShadow").css({"left": '0', "transition": "left 0.5s"});
  }, 1000);
  $(".slider-btn").css({"background-position": "0 -84px"});
  moveStart = '';
  $(".re-btn a").on("click", function () {
   Access.getAccess().initIdentifying($('#acessIdentifyingContent'));
  })
 }
}
/**
* 获取该类型验证码的一些参数
*/
ImgIdentifying.getParamMap = function () {
 var min_X = padding_ + pl_size;
 var max_X = width - padding_ - pl_size - pl_size / 6;
 var max_Y = padding_;
 var min_Y = height - padding_ - pl_size - pl_size / 6;
 var paramMap = new Map();
 paramMap.set("min_X", min_X);
 paramMap.set("max_X", max_X);
 paramMap.set("min_Y", min_Y);
 paramMap.set("max_Y", max_Y);
 return paramMap;
}
/**
* 设置验证成功的回调函数
* @param success
*/
ImgIdentifying.prototype.setSuccess = function (successFunc) {
 this.successFunc = successFunc;
}
/**
* 设置验证失败的回调函数
* @param success
*/
ImgIdentifying.prototype.setError = function (errorFunc) {
 this.errorFunc = errorFunc;
}

其中init的方法,大家就可以抄啦,验证码是这里生成的(感谢网上一些热心网友提供的Mod,在此基础上改的)。

2|0后端的设计:

 

首先要有一个验证码的接口,将一些常量和共同的方法抽象到接口中(接口最重要的作用就是行为的统一,意思是我如果知道这个是验证码,那么必定就会有验证的方法,不管它是滑动验证,图形验证等,然后就可以放心的调用验证方法去获取验证结果,下面过滤器设计就可以立马看到这作用。具体java接口的说明会单独写篇文章),接口如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/**
* 验证码类的接口,所有验证码必须继承此接口
*/
public interface I_Identifying<T> {
 String EXCEPTION_CODE = SystemStaticValue.IDENTIFYING_EXCEPTION_CODE;
 String IDENTIFYING = "Identifying";
 //--------------以下为验证码大体错误类型,抛出错误时候用,会传至前端---------------
 //验证成功
 String SUCCESS = "Success";
 //验证失败
 String FAILURE = "Failure";
 //验证码过期
 String OVERDUE = "Overdue";
 //-------以下为验证码具体错误类型,存放在checkResult-------------
 String PARAM_ERROR = "验证码参数错误";
 String OVERDUE_ERROR = "验证码过期";
 String TYPE_ERROR = "验证码业务类型错误";
 String ID_ERROR = "验证码id异常";
 String CHECK_ERROR = "验证码验证异常";
 /**
 * 获取生成好的验证码
 * @param request
 * @return
 */
 public T getInstance(HttpServletRequest request) throws Exception;
 /**
 * 进行验证,没抛异常说明验证无误
 * @return
 */
 public void checkIdentifying(HttpServletRequest request) throws Exception;
 /**
 * 获取验证结果,如果成功则为success,失败则为失败信息
 * @return
 */
 public String getCheckResult();
 /**
 * 获取验证码的业务类型
 * @return
 */
 public String getIdentifyingType();
}

然后,设计一个具体的滑动验证类去实现这个接口,这里只贴参数:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* @author NiceBin
* @description: 验证码类,前端需要生成验证码的信息
* @date 2019/7/12 16:04
*/
public class ImgIdentifying implements I_Identifying<ImgIdentifying>,Serializable {
 //此次验证码的id
 private String identifyingId;
 //此次验证码的业务类型
 private String identifyingType;
 //需要使用的图片
 private String imgSrc;
 //生成块的x坐标
 private int X;
 //生成块的y坐标
 private int Y;
 //允许的误差
 private int deviation = 2;
 //验证码生成的时间
 private Calendar calendar;
 //验证码结果,如果有结果说明已经被校验,防止因为网络延时的二次校验
 private String checkResult;
 
 //下面是逻辑代码...
}

上面每个变量都是一种校验手段,如calendar可以检验验证码是否过期,identifyingType检验此验证码是否是对应的业务等。每多想一点,别人破解就多费劲一点。

后端验证码的验证是不需要具体的类去调用的,而是被一个过滤器统一过滤,才过滤器注册的时候,将需要进行验证的路径写进去即可,过滤器代码如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
r NiceBin
 * @description: 验证码过滤器,帮忙验证有需要验证码的请求,不帮忙生成验证码
 * @date 2019/7/23 15:06
 */
 @Component
 public class IdentifyingInterceptor implements HandlerInterceptor {
  @Override
  public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
   HttpSession session = request.getSession();
   I_Identifying identifying= (I_Identifying)session.getAttribute(I_Identifying.IDENTIFYING);
   if(identifying!=null){
    identifying.checkIdentifying(request);
   }else {
    //应该携带验证码信息的,结果没有携带,那就是个非法请求
    return false;
   }
   return true;
  }
  @Override
  public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
  }
  @Override
  public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
  }
 }

 

可以看到接口的用处了,之前在用户申请验证码时,验证码类是放到用户session中的,所以这里直接取出调用checkIdentifying即可,不需要关系它到底是滑动验证码,还是图片验证码什么的。

总结

以上所述是小编给大家介绍的滑动验证码的设计与理解,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对服务器之家网站的支持!
如果你觉得本文对你有帮助,欢迎转载,烦请注明出处,谢谢!

原文链接:https://www.cnblogs.com/top-housekeeper/p/11392439.html