滑动验证码的设计与理解
简介
在介绍之前,首先一个概念明确一个共识:没有攻不破的网站,只有值不值得。
这意思是说,我们可以尽可能的提高自己网站的安全,但并没有绝对的安全,当网站安全级别大于攻击者能得到的回报时,你的网站就是安全的。
所以百度搜到的很多验证码都已经结合了人工智能分析用户行为,很厉害。但这里只介绍我的小网站是怎么设计的。
大概逻辑:当需要验证码时,前端发送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 }
可以看到接口的用处了,之前在用户申请验证码时,验证码类是放到用户session中的,所以这里直接取出调用checkIdentifying即可,不需要关心它到底是滑动验证码,还是图片验证码什么的。
以上就是对滑动验证的设计分享,都是自己拍脑袋想出来的和生产有差距,所以哪里不对或有更好的想法,欢迎大家留言讨论
如果对你有帮助,记得点赞呀~