滑動驗證碼的設計與理解
在介紹之前,首先一個概念明確一個共識:沒有攻不破的網站,只有值不值得。
這意思是說,我們可以儘可能的提高自己網站的安全,但並沒有絕對的安全,當網站安全級別大於攻擊者能得到的回報時,你的網站就是安全的。
所以百度搜到的很多驗證碼都已經結合了人工智慧分析使用者行為,很厲害。但這裡只介紹我的小網站是怎麼設計的。
大概邏輯:當需要驗證碼時,前端傳送ajax向後臺請求相關資料傳送回前端,由前端生成(與後端生成圖片,然後傳送圖片到前端的做法相比安全性要差很多。但也是可以預防的,後端可以對此Session進行請求記錄,如果在一定時間內惡意多次請求,可以進行封禁ip等對策),驗證完成後,後臺再對傳回的資料進行校驗。
效果圖:
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 }
然後,滑動驗證碼類繼承此父類(js繼承會單獨寫篇文章),滑動驗證碼類如下:
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即可,不需要關係它到底是滑動驗證碼,還是圖片驗證碼什麼的。
以上就是對滑動驗證的設計分享,都是自己拍腦袋想出來的和生產有差距,所以哪裡不對或有更好的想法,希望大家也分享給我,一起做個好朋友