滑动验证码的设计与理解

简介

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

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

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

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

效果图:

项目地址:https://www.top-housekeeper.xyz/welcome

 


 

js类的设计

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

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

然后,滑动验证码类继承此父类(继承详解:JavaScript的__proto__、prototype和继承),滑动验证码类如下:

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

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


 

后端的设计

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

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

 

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

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

 

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

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

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

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

 

以上就是对滑动验证的设计分享,都是自己拍脑袋想出来的和生产有差距,所以哪里不对或有更好的想法,欢迎大家留言讨论

如果对你有帮助,记得点赞呀~

posted @ 2019-08-22 10:59  Nice斌  阅读(5369)  评论(2编辑  收藏  举报